Tanto la composición como la herencia colocan subobjetos dentro de la nueva clase. Ambas usan la lista de inicialización del constructor para construir esos subobjetos. Puede que ahora se pregunte cuál es la diferencia entre ambas y cuándo elegir una u otra.
La composición se usa generalmente cuando se quieren las características de una clase existente dentro de la nueva clase, pero no su interfaz. Esto es, se aloja un objeto para implementar características de la nueva clase, pero el usuario de la nueva clase ve la interfaz que usted ha definido, en vez de la interfaz de la clase original. Para ello se sigue el camino habitual de alojar objetos privados de clases existentes dentro de la nueva clase.
En ocasiones, sin embargo, tiene sentido permitir que el usuario de la clase acceda directamente a la composición de la nueva clase, esto es, hacer públicos los objetos miembro. Los objetos miembro aplican su propio control de acceso, así que hacerlo es seguro, y cuando el usuario sabe que se está ensamblando un conjunto de piezas, la interfaz resulta más fácil de entender. Una clase Car es un buen ejemplo:
//: 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 un Car forma parte del análisis del problema (y no simplemente del diseño subyacente), hacer públicos los miembros ayuda al programador cliente a entender cómo se utiliza la clase y requiere un código menos complejo por parte del creador de la clase.
Si lo piensa un poco, verá también que no tendría 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 se expresa con la herencia, y la relación tiene-un se expresa con la composición.
Suponga ahora que desea crear un tipo de objeto ifstream que no sólo abra un fichero, sino que también guarde el nombre del fichero. Puede usar la composición y alojar tanto un ifstream como 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, aquí hay un problema. Se intenta permitir el uso del objeto FName1 en cualquier lugar donde se utilice un objeto ifstream incluyendo un operador de conversión automática de tipo de FName1 a ifstream&. Pero en main(), la línea
file.close();
no compilará porque la conversión automática de tipo sólo ocurre en las llamadas a funciones, no durante la selección de miembros. Por ello, este enfoque no funcionará.
Un segundo enfoque es añadir la definición de close() a FName1:
void close() { file.close(); }
Esto funcionará si sólo hay unas pocas funciones de la clase ifstream que quiera trasladar. En ese caso sólo está utilizando una parte de la clase, y la composición es apropiada.
Pero ¿y si quiere trasladar todo lo que hay en la clase? A esto se le llama subtipado, porque está creando un nuevo tipo a partir de un tipo existente y quiere que el nuevo tipo tenga exactamente la misma interfaz que el tipo existente (más cualquier otra función miembro que desee añadir), de modo que se pueda utilizar en cualquier lugar donde se utilizaría el tipo existente. Aquí es donde la herencia es esencial. Puede ver que el subtipado resuelve perfectamente el problema del ejemplo 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 miembro disponible para un objeto ifstream también está disponible para un objeto FName2. También puede ver que funciones no miembro como getline(), que esperan un ifstream, pueden funcionar igualmente con un FName2. Esto se debe a que un FName2 es un tipo de ifstream; no se limita a contener uno. Éste es un asunto muy importante que se explorará al final de este capítulo y en el siguiente.
Puede heredar de una clase base de forma privada omitiendo public en la lista de clases base, o indicando explícitamente private (probablemente una política mejor, porque deja claro al usuario que eso es lo que pretende). Cuando hereda de forma privada, está «implementando en términos de»; esto es, está creando una nueva clase que tiene todos los datos y la funcionalidad de la clase base, pero esa funcionalidad está oculta, de modo que sólo forma parte de la implementación subyacente. El usuario de la clase no tiene acceso a la funcionalidad subyacente, y un objeto no puede tratarse como una instancia de la clase base (como ocurría en FName2.cpp).
Puede que se pregunte cuál es el propósito de la herencia privada, porque la alternativa de usar la composición para crear un objeto privado en la nueva clase parece más apropiada. La herencia privada se incluye en el lenguaje para que esté completo, pero aunque sólo sea para reducir la confusión, normalmente preferirá usar la composición en vez de la herencia privada. Sin embargo, en ocasiones puede haber situaciones en las que quiera ofrecer parte de la misma interfaz que la clase base e impedir que el objeto se trate como si fuera un objeto de la clase base. La herencia privada proporciona esa posibilidad.
Cuando se hereda de forma privada, todos los miembros públicos de la clase base pasan a ser privados. Si desea que alguno de ellos sea visible, basta con indicar su nombre (sin argumentos ni valores de retorno) junto con la palabra reservada using en la 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 en que exponer el nombre de una función sobrecargada expone todas las versiones de esa función sobrecargada en la clase base.
Debe pensarlo detenidamente antes de utilizar la herencia privada en vez de la composición; la herencia privada tiene complicaciones particulares cuando se combina con la identificación de tipos en tiempo de ejecución (éste es el tema de un capítulo del Volumen 2 de este libro, que se puede descargar de www.BruceEckel.com).