14.5.2. Composición vs. herencia

La composición y la herencia colocan subobjetos dentro de la clase. Ambos usan la lista de inicialización del constructor para construir esos subobjetos. Pero se preguntará cuál es la diferencia entre los dos, y cuando escoger una y no la otra.

La composición generalmente se usa cuando se quieren las características de una clase existente dentro se su clase, pero no en su interfaz. Esto es, aloja un objeto para implementar características en su clase, pero el usuario de su clase ve el interfaz que se ha definido, en vez del interfaz de la clase original. Para hacer esto, se sigue el típico patrón de alojar objetos privados de clases existentes en su nueva clase.

En ocasiones, sin embargo, tiene sentido permitir que el usuario de la clase acceda a la composición de su clase, esto es, hacer públicos los miembros objeto. Los miembros objeto usan su control de accesos, entonces es seguro y cuando el usuario conoce que esta formando un conjunto de piezas, hace que la interfaz sea más fácil de entender. Un buen ejemplo es la clase Car:

//: C14:Car.cpp
// Public composition

class Engine {
public:
  void start() const {}
  void rev() const {}
  void stop() const {}
};

class Wheel {
public:
  void inflate(int psi) const {}
};

class Window {
public:
  void rollup() const {}
  void rolldown() const {}
};

class Door {
public:
  Window window;
  void open() const {}
  void close() const {}
};

class Car {
public:
  Engine engine;
  Wheel wheel[4];
  Door left, right; // 2-door
};

int main() {
  Car car;
  car.left.window.rollup();
  car.wheel[0].inflate(72);
} ///:~

Listado 14.11. C14/Car.cpp


Como la composición de Car es parte del análisis del problema (y no una simple capa del diseño), hace públicos los miembros y ayudan al programador a entender como se utiliza la clase y requiere menos complejidad de código para el creador de la clase.

Si piensa un poco, observará que no tiene sentido componer un Car usando un objeto "vehículo" - un coche no contiene un vehículo, es un vehículo. La relación "es-un" es expresado con la herencia y la relación "tiene un" es expresado con la composición.

Subtipado

Ahora suponga que desea crear un objeto del tipo ifstream que no solo abre un fichero sino que también guarde el nombre del fichero. Puede usar la composición e alojar un objeto ifstream y un string en la nueva clase:

//: C14:FName1.cpp
// An fstream with a file name
#include "../require.h"
#include <iostream>
#include <fstream>
#include <string>
using namespace std;

class FName1 {
  ifstream file;
  string fileName;
  bool named;
public:
  FName1() : named(false) {}
  FName1(const string& fname) 
    : fileName(fname), file(fname.c_str()) {
    assure(file, fileName);
    named = true;
  }
  string name() const { return fileName; }
  void name(const string& newName) {
    if(named) return; // Don't overwrite
    fileName = newName;
    named = true;
  }
  operator ifstream&() { return file; }
};

int main() {
  FName1 file("FName1.cpp");
  cout << file.name() << endl;
  // Error: close() not a member:
//!  file.close();
} ///:~

Listado 14.12. C14/FName1.cpp


Sin embargo, existe un problema. Se intenta permitir el uso de un objeto FName1 en cualquier lugar dónde se utilice un objeto ifstream, incluyendo una conversión automática del tipo desde FName1 a ifstream&. Pero en main, la línea

file.close();

no compilará porque la conversión automática de tipo sólo ocurre cuando se llama a la función, no durante la selección del miembro. Por ello, esta manera no funcionará.

Una segunda manera es añadir la definición Close() a FName1:

void close() { file.close(); }

Esto funcionará si sólo existen unas cuantas funciones a las que se desea hacer funcionar como una clase ifstream. En este caso, solo una parte de la clase y la composición apropiada.

Pero ¿qué ocurre si se quiere que todo funcione cómo la clase deseada? A eso se le llama subtipos porque esta creando un nuevo tipo desde uno ya existente y lo que se quiere es que el nuevo tipo tenga la misma interfaz que el tipo existente (además de otras funciones que se deseen añadir) para que se pueda utilizar en cualquier lugar donde se utilizaba el tipo existente. Aquí es dónde la herencia es esencial. Puede ver que el subtipo resuelve perfectamente el problema anterior:

//: C14:FName2.cpp
// Subtyping solves the problem
#include "../require.h"
#include <iostream>
#include <fstream>
#include <string>
using namespace std;

class FName2 : public ifstream {
  string fileName;
  bool named;
public:
  FName2() : named(false) {}
  FName2(const string& fname)
    : ifstream(fname.c_str()), fileName(fname) {
    assure(*this, fileName);
    named = true;
  }
  string name() const { return fileName; }
  void name(const string& newName) {
    if(named) return; // Don't overwrite
    fileName = newName;
    named = true;
  }
};

int main() {
  FName2 file("FName2.cpp");
  assure(file, "FName2.cpp");
  cout << "name: " << file.name() << endl;
  string s;
  getline(file, s); // These work too!
  file.seekg(-200, ios::end);
  file.close();
} ///:~

Listado 14.13. C14/FName2.cpp


Ahora cualquier función que este disponible para el objeto sfstream también esta disponible para el objeto FName2. Asimismo se observan funciones no miembro como getline() que esperan un objeto ifstream y que pueden funcionar con un objeto FName2. Esto es porque FName2 es un tipo de ifstream; esto no significa simplemente que lo contiene. Esto es un tema muy importante que será explorado al final de este capitulo y el siguiente.

Herencia privada

Puede heredar utilizando una clase base de forma privada borrando public en la lista de la clase base o explícitamente utilizando private (definitivamente la mejor política a tomar pues indica al usuario lo que desea hacer). Cuando se hereda de forma privada, esta "implementado en términos de", esto es, se esta creando una nueva clase que tiene todos los datos y funcionalidad de la clase base, pero la funcionalidad esta oculta, solo una parte de capa de implementación. La clase derivada no tiene acceso a la capa de funcionalidad y un objeto no puede ser creado como instancia de la clase base (como ocurrió en FName2.cpp).

Se sorprenderá del propósito de la herencia privada, porque la alternativa, usar la composición para crear un objeto privado en la nueva clase parece más apropiada. La herencia privada esta incluida para completar el lenguaje pero para reducir confusión, normalmente se usará la composición en vez de la herencia privada. Sin embargo, existen ocasiones donde se desea el mismo interfaz como la clase base y anular tratamiento del objeto. La herencia privada proporciona esta habilidad.

Publicar los miembros heredados de forma privada

Cuando se hereda de forma privada, todos los miembros públicos de la clase base llegan como privados. Si desea que cualquiera de ellos sea visible, solo use sus nombres (sin argumentos o valores de retorno) junto con la palabra clave using en una sección pública de la clase derivada:

//: C14:PrivateInheritance.cpp
class Pet {
public:
  char eat() const { return 'a'; }
  int speak() const { return 2; }
  float sleep() const { return 3.0; }
  float sleep(int) const { return 4.0; }
};

class Goldfish : Pet { // Private inheritance
public:
  using Pet::eat; // Name publicizes member
  using Pet::sleep; // Both members exposed
};

int main() {
  Goldfish bob;
  bob.eat();
  bob.sleep();
  bob.sleep(1);
//! bob.speak();// Error: private member function
} ///:~

Listado 14.14. C14/PrivateInheritance.cpp


Así, la herencia privada es útil si desea esconder parte de la funcionalidad de la clase base.

Fíjese que si expone el nombre de una función sobrecargada, expone todas las versiones sobrecargadas de la función en la clase base.

Debe pensar detenidamente antes de utilizar la herencia privada en vez de la composición; la herencia privada tiene complicaciones particulares cuando son combinadas con la identificación de tipos en tiempo de ejecución (es un tema de un capítulo en el volumen 2, disponible en www.BruceEckel.com)