Composición vs. Herencia
Parte de la sección Programación Orientada a Objetos del Journey de C++ de Coddy — lección 76 de 104.
Al diseñar relaciones entre clases, tienes dos opciones principales: herencia ("es-un") y composición ("tiene-un"). Elegir el enfoque correcto impacta significativamente la flexibilidad y el mantenimiento de tu código.
La herencia crea un acoplamiento fuerte entre clases. Un Car que hereda de Vehicle está permanentemente ligado a esa relación:
class Vehicle {
public:
virtual void start() { std::cout << "Starting vehicle\n"; }
};
class Car : public Vehicle {
public:
void start() override { std::cout << "Starting car\n"; }
};La composición incrusta objetos como miembros, creando una relación más flexible. Un Car tiene un Engine en lugar de ser un motor:
class Engine {
public:
void start() { std::cout << "Engine running\n"; }
};
class Car {
private:
Engine engine; // El Coche TIENE un Motor
public:
void start() { engine.start(); }
};La ventaja clave de la composición es la flexibilidad. Puedes intercambiar componentes fácilmente, cambiar el comportamiento en tiempo de ejecución o combinar múltiples capacidades sin las restricciones de una jerarquía de clases rígida. El diseño moderno de C++ favorece la composición sobre la herencia en la mayoría de los casos.
Usa la herencia cuando haya una relación genuina de tipo "es-un" y necesites polimorfismo. Usa la composición cuando quieras reutilizar funcionalidad o cuando la relación se describa mejor como "tiene-un" o "usa-un".
Desafío
FácilVamos a construir un simulador de sistema informático que demuestre cuándo usar composición en lugar de herencia. Modelarás una computadora que tiene componentes (composición) en lugar de ser un componente, mostrando cómo la composición proporciona flexibilidad para intercambiar piezas en tiempo de ejecución.
Organizarás tu código en tres archivos:
Components.h: Define los componentes individuales que una computadora puede contener.Crea una clase
CPUcon unstd::string modely unint coresprivados. Proporciona un constructor que reciba ambos valores, un métodoprocess()que imprimaCPU [model] processing with [cores] cores, y un métodogetModel().Crea una clase
Memorycon unint sizeGBprivado. Proporciona un constructor, un métodoload()que imprimaLoading data into [sizeGB]GB RAM, y un métodogetSize().Crea una clase
Storagecon unstd::string typeprivado (como "SSD" o "HDD") y unint capacityGBprivado. Proporciona un constructor, un métodoread()que imprimaReading from [capacityGB]GB [type], y métodos getter para ambos campos.Computer.h: Define una claseComputerque utiliza la composición para combinar componentes.Tu
Computerdebe tener objetos miembros privados: unCPU, unaMemoryy unStorage. Utiliza la lista de inicialización del constructor para inicializar estos componentes a partir de los parámetros.Implementa estos métodos:
boot()— imprimeBooting computer..., luego llama aload()en la memoria,read()en el almacenamiento yprocess()en la CPU, cada uno en líneas separadas.specs()— imprime las especificaciones de la computadora en este formato:Computer Specs: - CPU: [model] ([cores] cores) - RAM: [size]GB - Storage: [capacity]GB [type]
main.cpp: Lee seis entradas (cada una en una línea separada):- Nombre del modelo de CPU (string)
- Número de núcleos de CPU (integer)
- Tamaño de la memoria en GB (integer)
- Tipo de almacenamiento (string, por ejemplo, "SSD")
- Capacidad de almacenamiento en GB (integer)
- Un comando: ya sea "boot" o "specs"
Crea los objetos de los componentes, luego crea un
Computerusando composición. Basándote en el comando, llama aboot()o aspecs().
Por ejemplo, con las entradas Intel i7, 8, 16, SSD, 512, y boot:
Booting computer...
Loading data into 16GB RAM
Reading from 512GB SSD
CPU Intel i7 processing with 8 coresCon las entradas AMD Ryzen, 6, 32, HDD, 1000, y specs:
Computer Specs:
- CPU: AMD Ryzen (6 cores)
- RAM: 32GB
- Storage: 1000GB HDDObserva cómo la clase Computer no hereda de ningún componente; los contiene. Esta relación de "tiene-un" significa que podrías crear fácilmente computadoras con diferentes combinaciones de componentes, o incluso intercambiar componentes más tarde si añadieras métodos setter. Esta flexibilidad es la ventaja clave de la composición sobre la herencia.
Hoja de referencia
Al diseñar relaciones entre clases, elija entre herencia ("es-un") y composición ("tiene-un").
La herencia crea un acoplamiento fuerte entre clases:
class Vehicle {
public:
virtual void start() { std::cout << "Starting vehicle\n"; }
};
class Car : public Vehicle {
public:
void start() override { std::cout << "Starting car\n"; }
};La composición incrusta objetos como miembros, creando relaciones flexibles:
class Engine {
public:
void start() { std::cout << "Engine running\n"; }
};
class Car {
private:
Engine engine; // El Car TIENE UN Engine
public:
void start() { engine.start(); }
};Cuándo usar cada una:
- Use herencia para relaciones genuinas de tipo "es-un" cuando necesite polimorfismo
- Use composición para reutilizar funcionalidad o para relaciones de tipo "tiene-un"/"usa-un"
- El diseño moderno de C++ favorece la composición por su flexibilidad: puede intercambiar componentes, cambiar el comportamiento en tiempo de ejecución y combinar capacidades sin jerarquías rígidas
Pruébalo tú mismo
#include <iostream>
#include <string>
#include "Computer.h"
using namespace std;
int main() {
// Leer entradas
string cpuModel;
getline(cin, cpuModel); // Nombre del modelo de CPU (puede contener espacios)
int cpuCores;
cin >> cpuCores; // Número de núcleos de CPU
int memorySize;
cin >> memorySize; // Tamaño de la memoria en GB
string storageType;
cin >> storageType; // Tipo de almacenamiento (por ejemplo, "SSD")
int storageCapacity;
cin >> storageCapacity; // Capacidad de almacenamiento en GB
string command;
cin >> command; // Comando: "boot" o "specs"
// TODO: Crear objetos de componentes (CPU, Memory, Storage)
// TODO: Crear un objeto Computer usando composición
// TODO: Basado en el comando, llamar a boot() o specs()
return 0;
}
Esta lección incluye un breve cuestionario. Empieza la lección para responderlo y registrar tu progreso.
Todas las lecciones de Programación Orientada a Objetos
1Fundamentos de OOP
Archivos externosConstrucción y compilación en C++Archivos de cabecera y archivos fuenteNamespaces y alcanceIntroducción a OOP en C++Clases vs ObjetosEl puntero 'this'Métodos (Funciones miembro)Atributos (Miembros de datos)Conceptos básicos de Ctors y DtorsResumen - Calculadora simple4Propiedades de clase
Miembros de instancia vs. estáticosGetters y SettersFunciones miembro constPalabra clave mutableMétodos y variables estáticosFunciones y clases amigasResumen - Gestor de cuentas bancarias7Herencia
Herencia básicaNiveles de acceso en la herenciaOrden de llamada de Ctor y DtorSobrescritura de métodosFunciones virtuales y VTableHerencia múltipleHerencia virtualResumen - Jerarquía de empleados2Gestión de memoria
Memoria Stack vs HeapPunteros y referenciasMemoria dinámica (new/delete)Punteros inteligentes en C++RAII en C++Resumen - Gestor de arrays dinámicos5Encapsulamiento
Especificadores de acceso en C++Especificadores de acceso en profundidadOcultamiento de informaciónStruct vs ClassClases anidadas e internasResumen - Sistema de registros de estudiantes8Polimorfismo
Polimorfismo: Compilación vs. Tiempo de ejecuciónSobrecarga de funcionesFunciones virtuales revisadasFunciones virtuales purasClases abstractasDiseño de interfaces en C++Dynamic Casting y RTTIResumen: Calculadora de figuras11Conceptos avanzados de POO
Composición vs. HerenciaMixins mediante CRTPIdioma PimplBorrado de tiposEnum Classes y tipado fuerteManejo de excepciones en POOJerarquías de excepciones personalizadas14Patrones de diseño - Parte 2
Patrón CommandPatrón AdapterPatrón DecoratorPatrón Template MethodPatrón StatePatrón CompositeRAII como patrón3Constructores y Destructores
Constructor por defectoConstructor parametrizadoConstructor de copiaConstructor de movimientoListas de inicialización del constructorConstructores delegadosAnálisis profundo del destructorRegla de tres / cinco / ceroResumen - Clase String6Sobrecarga de operadores
Introducción a la sobrecarga de operadoresSobrecarga de operadores aritméticosSobrecarga de operadores de comparaciónOperadores de flujo (Stream)Sobrecarga del operador de asignaciónSobrecarga de los operadores [] y ()Operadores de conversión de tiposResumen - Clase Matrix9Plantillas
Plantillas de funcionesPlantillas de clasesEspecialización de plantillasPlantillas variádicasConceptos básicos de SFINAE y Type TraitsResumen - Contenedor genérico