Composición vs. Herencia
Parte de la sección Programación Orientada a Objetos del Journey de Java de Coddy — lección 47 de 87.
La herencia crea una relación "es-un": un Dog es un Animal. Pero, ¿qué pasa si una clase necesita funcionalidad de algo que no es? Aquí es donde entra la composición, estableciendo en su lugar una relación "tiene-un".
Con la composición, una clase contiene instancias de otras clases como campos en lugar de extenderlas:
// Enfoque de herencia: Car ES UN Engine (no tiene sentido)
class Car extends Engine { }
// Enfoque de composición: Car TIENE UN Engine (tiene sentido)
class Car {
private Engine engine;
public Car(Engine engine) {
this.engine = engine;
}
public void start() {
engine.ignite();
}
}La composición ofrece varias ventajas. Puedes cambiar el objeto compuesto en tiempo de ejecución, como intercambiar motores.
También puedes combinar comportamientos de múltiples clases, algo que la herencia no permite en Java. La clase compuesta solo expone los métodos que elige, proporcionando una mejor encapsulación.
Una guía práctica: utiliza la herencia cuando exista una relación genuina de tipo "es-un" y la subclase represente verdaderamente una versión especializada del padre.
Usa la composición cuando necesites utilizar la funcionalidad de otra clase sin ser de ese tipo. Muchos desarrolladores experimentados siguen el principio: "favorecer la composición sobre la herencia" porque conduce a un código más flexible y fácil de mantener.
Desafío
FácilVamos a construir un sistema informático utilizando la composición para demostrar la relación "tiene-un" (has-a). En lugar de que un Computer herede de sus componentes (lo cual no tendría sentido), tu computadora contendrá las partes que necesita para funcionar.
Organizarás tu código en tres archivos:
Processor.java: Crea una clase que represente una CPU. Un Processor tiene dos campos privados:brand(String) yspeedGHz(double). Incluye un constructor para inicializar ambos campos y métodos getter para cada uno. Añade un métodoprocess()que devuelva un String:[brand] processing at [speedGHz] GHzMemory.java: Crea una clase que represente la RAM. Una Memory tiene dos campos privados:type(String, como "DDR4" o "DDR5") ysizeGB(int). Incluye un constructor, getters y un métodoload()que devuelva:Loading [sizeGB]GB [type] memoryComputer.java: ¡Aquí es donde brilla la composición! Tu clase Computer debe tener un Processor y una Memory como campos privados; no los extiende, los contiene. Incluye un constructor que acepte ambos componentes. Añade un métodoboot()que devuelva un String multilínea que muestre la computadora iniciándose mediante el uso de sus componentes:También añade un métodoBooting computer... [processor.process() result] [memory.load() result] System ready!getSpecs()que devuelva:Specs: [processor brand] CPU, [memory sizeGB]GB [memory type]Main.java: ¡Da vida a tu sistema compuesto! Recibirás cuatro entradas: marca del procesador (String), velocidad del procesador (double), tipo de memoria (String) y tamaño de la memoria (int).Crea un Processor y una Memory con estos valores, luego compónlos en un Computer. Imprime el resultado de llamar a
boot(), luego imprime el resultado degetSpecs().
Recibirás cuatro entradas en orden: marca del procesador, velocidad del procesador (GHz), tipo de memoria y tamaño de la memoria (GB).
Observa cómo el Computer delega el trabajo a sus componentes en lugar de intentar ser ellos; ¡este es el poder de la composición! El Computer puede usar cualquier Processor o Memory que le proporciones, lo que hace que el diseño sea flexible y realista.
Hoja de referencia
La composición crea una relación "tiene un" (has-a) donde una clase contiene instancias de otras clases como campos en lugar de extenderlas.
Herencia vs Composición:
// Herencia: Car ES UN Engine (no tiene sentido)
class Car extends Engine { }
// Composición: Car TIENE UN Engine (tiene sentido)
class Car {
private Engine engine;
public Car(Engine engine) {
this.engine = engine;
}
public void start() {
engine.ignite();
}
}Ventajas de la Composición:
- Los objetos se pueden cambiar en tiempo de ejecución
- Puede combinar comportamientos de múltiples clases
- Mejor encapsulación: solo expone los métodos elegidos
- Código más flexible y fácil de mantener
Cuándo usar:
- Herencia: Úsela cuando haya una relación genuina "es un" (is-a) y la subclase sea una versión especializada del padre
- Composición: Úsela cuando necesite la funcionalidad de otra clase sin ser de ese tipo
Mejor práctica: "Prefiera la composición sobre la herencia"
Pruébalo tú mismo
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// Leer entradas
String processorBrand = scanner.nextLine();
double processorSpeed = Double.parseDouble(scanner.nextLine());
String memoryType = scanner.nextLine();
int memorySize = Integer.parseInt(scanner.nextLine());
// TODO: Crear un Processor con la marca y la velocidad
// TODO: Crear una Memory con el tipo y el tamaño
// TODO: Crear un Computer componiendo el Processor y la Memory
// TODO: Imprimir el resultado de boot()
// TODO: Imprimir el resultado de getSpecs()
}
}
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 externosIntroducción a la OOPClases vs. ObjetosLa palabra clave thisMétodosCampos (Atributos)Método constructorSobrecarga de constructoresResumen - Calculadora simple4Herencia
Herencia básica (extends)La palabra clave superSobrescritura de métodos (@Override)Encadenamiento de constructoresLa clase ObjectHerencia simple y multinivelPor qué no hay herencia múltiple de clasesResumen - Jerarquía de empleados7Métodos especiales y la clase Object
Método toString()equals() y hashCode()Método clone()compareTo() y ComparableInterfaz ComparatorResumen - Ordenamiento personalizado2Modificadores de acceso y encapsulamiento
Visión general de los niveles de accesoMétodos Getter y SetterOcultamiento de informaciónLa palabra clave finalResumen - Gestor de cuentas bancarias5Polimorfismo
Fundamentos de sobrecarga de métodosSobrescritura de métodos (tiempo de ejecución)Upcasting y DowncastingEl operador instanceofClases y métodos abstractosResumen - Calculadora de figuras8Conceptos avanzados de POO
Composición vs. HerenciaAgregación vs. ComposiciónClases internas, anidadas y anónimasEnums y métodos de EnumRecords (Java 16+)Clases selladas (Java 17+)11Patrones de diseño - Parte 1
Introducción a los patrones de diseñoPatrón SingletonPatrón FactoryPatrón BuilderPatrón ObserverPatrón Strategy3Propiedades de clase y miembros estáticos
Variables de instancia vs. estáticasMétodos estáticosBloques estáticosConstantes (static final)Repaso - Contador y utilidad6Interfaces y Clases Abstractas
Introducción a las InterfacesImplementación de InterfacesImplementación de Múltiples InterfacesMétodos Default y Static en InterfacesClases Abstractas vs InterfacesInterfaces FuncionalesResumen - Sistema de Pagos9Genéricos
Introducción a los genéricosClases genéricasMétodos genéricosParámetros de tipo delimitadosComodines (?, extends, super)Resumen - Contenedor genérico12Patrones de diseño Parte 2
Patrón CommandPatrón AdapterPatrón DecoratorPatrón Template MethodPatrón StatePatrón CompositePatrón Iterator