Composición vs. Herencia
Parte de la sección Programación Orientada a Objetos del Journey de Dart de Coddy — lección 82 de 110.
Has aprendido que la herencia crea una relación "es-un" - un Dog es un Animal. Pero existe otro enfoque poderoso: la composición, que crea una relación "tiene-un". En lugar de heredar el comportamiento, una clase contiene instancias de otras clases y les delega el trabajo.
Considera una clase Car. Con la herencia, podrías intentar extender una clase Engine, pero un coche no es un motor, tiene un motor:
class Engine {
void start() => print('Engine started');
}
class Car {
final Engine _engine = Engine(); // Composición: Car TIENE un Engine
void start() {
_engine.start(); // Delegar al engine
print('Car is ready to drive');
}
}
void main() {
var car = Car();
car.start();
}La ventaja clave de la composición es la flexibilidad. Con la herencia, estás limitado a una única clase padre. Con la composición, puedes combinar múltiples componentes e incluso intercambiarlos en tiempo de ejecución:
class ElectricEngine {
void start() => print('Electric motor humming');
}
class HybridCar {
Engine? _gasEngine;
ElectricEngine? _electricEngine;
HybridCar({bool useElectric = false}) {
if (useElectric) {
_electricEngine = ElectricEngine();
} else {
_gasEngine = Engine();
}
}
}Una pauta común es "favorecer la composición sobre la herencia". Usa la herencia cuando exista una verdadera relación "es-un" y necesites polimorfismo. Usa la composición cuando quieras reutilizar el comportamiento sin un acoplamiento fuerte, o cuando un objeto necesite capacidades de múltiples fuentes.
Desafío
Fácil¡Vamos a construir un sistema informático utilizando la composición! En lugar de intentar que una computadora herede de sus partes, crearás clases de componentes separadas y las compondrás juntas, porque una computadora tiene una CPU y tiene memoria, no es una CPU ni una memoria.
Organizarás tu código en tres archivos:
components.dart: Crea dos clases de componentes que representen partes de la computadora:- Una clase
CPUcon unString brandy undouble speedGHz. Incluye un métodoprocess()que devuelva un string:'$brand processing at ${speedGHz}GHz' - Una clase
Memorycon unint sizeGB. Incluye un métodoload()que devuelva un string:'Loading ${sizeGB}GB of data'
- Una clase
computer.dart: Crea una claseComputerque utilice la composición para combinar los componentes. Tu computadora debe:- Tener un campo
String name - Contener una instancia de
CPUy una deMemory(composición: la computadora TIENE estas partes) - Aceptar todos los valores necesarios a través de su constructor para crear ambos componentes internamente
- Tener un método
boot()que delegue en ambos componentes e imprima tres líneas: el nombre de la computadora seguido de' starting...', luego el resultado del método process de la CPU, y después el resultado del método load de la Memory - Tener un método
specs()que devuelva un string describiendo el sistema:'$name: ${cpu.brand} ${cpu.speedGHz}GHz, ${memory.sizeGB}GB RAM'
- Tener un campo
main.dart: Importa tus archivos y demuestra la composición en acción:- Crea un
Computerllamado'Workstation'con una CPU'Intel'funcionando a3.5GHz y16GB de memoria - Llama al método
boot() - Imprime el resultado de
specs()
- Crea un
Observa cómo la clase Computer no extiende de CPU ni de Memory; los contiene y les delega el trabajo. ¡Esta es la relación "tiene-un" (has-a) que crea la composición!
Salida esperada:
Workstation starting...
Intel processing at 3.5GHz
Loading 16GB of data
Workstation: Intel 3.5GHz, 16GB RAMHoja de referencia
La composición crea una relación de tipo "tiene un" (has-a) donde una clase contiene instancias de otras clases y les delega el trabajo, en lugar de heredar de ellas.
Ejemplo básico de composición:
class Engine {
void start() => print('Engine started');
}
class Car {
final Engine _engine = Engine(); // Car HAS an Engine
void start() {
_engine.start(); // Delegate to the engine
print('Car is ready to drive');
}
}Composición con flexibilidad - intercambio de componentes:
class ElectricEngine {
void start() => print('Electric motor humming');
}
class HybridCar {
Engine? _gasEngine;
ElectricEngine? _electricEngine;
HybridCar({bool useElectric = false}) {
if (useElectric) {
_electricEngine = ElectricEngine();
} else {
_gasEngine = Engine();
}
}
}Cuándo usar composición frente a herencia:
- Usa la herencia para relaciones verdaderas de tipo "es un" (is-a) cuando necesites polimorfismo
- Usa la composición para reutilizar el comportamiento sin un acoplamiento fuerte, o cuando un objeto necesite capacidades de múltiples fuentes
- Guía general: "prioriza la composición sobre la herencia"
Pruébalo tú mismo
import 'computer.dart';
void main() {
// TODO: Crea un Computer llamado "Workstation" con:
// - CPU Intel funcionando a 3.5 GHz
// - 16 GB de memoria
// TODO: Llama al método boot()
// TODO: Imprime el resultado de specs()
}
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 POO
Archivos externosLibrerías e importacionesIntroducción a la POOClases vs. ObjetosLa palabra clave thisMétodosVariables de instanciaConceptos básicos de constructoresResumen - Calculadora sencilla4Seguridad de nulos
Introducción a la seguridad de nulosNulables vs No nulablesLos operadores ? y !Palabra clave Late y Null SafetyOperadores Null-awareNull Safety en clasesResumen - Sistema de perfil de usuario7Clases abstractas e interfaces
Clases abstractasMétodos abstractosInterfaces en DartInterfaces implícitasImplementar vs. ExtenderInterfaces múltiplesRepaso - Calculadora de figuras10Colecciones y Genéricos
Visión general de List, Set y MapColecciones con seguridad de tiposClases genéricasMétodos genéricosRestricciones genéricasIterable e IteratorResumen - Almacenamiento genérico13Conceptos avanzados de POO
Composición vs. HerenciaMétodos de extensiónClases invocablesClases selladas (Dart 3)Records (Dart 3)Patrones y coincidencia (3.0)Enums con métodos2Constructores en Dart
Constructor por defectoConstructores nombradosListas de inicializaciónConstructores constantesConstructores de fábricaConstructores de redireccionamientoResumen - Constructor de formas5Encapsulamiento
Miembros públicos vs. privadosLa convención del prefijo _Privacidad a nivel de libreríaProfundidad en Getters y SettersOcultamiento de informaciónResumen - Registros de estudiantes8Mixins
Introducción a los MixinsCreación de MixinsUso de múltiples MixinsPalabra clave on en MixinsMixin vs HerenciaMixin vs InterfazResumen - Sistema Animal11Métodos especiales
Sobrescritura de toString()Sobrescritura de hashCode y ==Interfaz ComparableMétodo call()Sobrescritura de noSuchMethodResumen - Colección personalizada14Patrones de diseño Parte 1
Introducción a los patrones de diseñoPatrón SingletonPatrón FactoryPatrón ObserverPatrón Strategy3Propiedades de clase
Miembros de instancia vs. estáticosCampos Final y ConstVariables LateMétodos y campos estáticosGetters y SettersResumen - Gestor de cuentas bancarias6Herencia
Herencia básicaLa palabra clave superSobrescritura de métodosLa anotación @overrideLa palabra clave final en clasesConstructores y herenciaResumen - Jerarquía de empleados9Polimorfismo
Fundamentos del polimorfismoPolimorfismo mediante interfacesComprobación de tipos en tiempo de ejecuciónLos operadores is y asPalabra clave covariantResumen - Procesador de pagos12POO Asíncrona
Futures y async/awaitFundamentos de StreamsStream ControllersConstructores asíncronosAsincronía en métodos de claseRepaso - Data Fetcher15Patrones de diseño Parte 2
Patrón CommandPatrón AdapterPatrón DecoratorPatrón Template MethodPatrón StatePatrón CompositePatrón Repository