Composición vs Herencia
Gino Luraschi
Posted on September 25, 2024
Introducción
La herencia y la composición son dos conceptos fundamentales en la programación orientada a objetos (POO), pero se utilizan de manera diferente y tienen propósitos distintos. El objetivo de este post es repasar esos propositos, y algunas cosas a tener en cuenta a la hora de elegirlos.
Conceptos de la herencia
Cuando pensemos en aplicar la herencia en nuestros diseños, tenemos que entender:
- Definición: En la herencia, una clase (llamada clase derivada o subclase) puede heredar propiedades y comportamientos de otra clase (llamada clase base o superclase). La clase derivada puede extender o modificar la funcionalidad de la clase base.
- Relación: Es una relación "es un" (is-a). Por ejemplo, si tienes una clase "Vehículo" y otra clase "Automóvil", la clase "Automóvil" es una subclase de "Vehículo".
- Ventajas: Promueve la reutilización de código y permite la extensión fácil de funcionalidades.
Conceptos de la composición
En cambio, si pensamos componer objetos entre sí:
- Definición: En la composición, un objeto contiene otros objetos y delega parte de su funcionalidad a ellos. En lugar de heredar, una clase utiliza instancias de otras clases para lograr su funcionalidad.
- Relación: Es una relación "tiene un" (has-a). Por ejemplo, si tienes una clase "Motor" y una clase "Automóvil", la clase "Automóvil" puede tener un objeto de la clase "Motor".
- Ventajas: Mayor flexibilidad y menor acoplamiento entre clases. Cambios en una clase no afectan directamente a la otra.
Por qué composición sobre herencia?
La pregunta de si la composición es mejor que la herencia o viceversa es un tema debatido en el diseño de software. Ambos enfoques tienen sus ventajas y desventajas, y la elección depende del contexto y los requisitos específicos del proyecto. Aquí te presentaré un ejemplo donde la composición puede ser preferible a la herencia.
Vamos a explorar un ejemplo en Java que ilustre cómo la composición puede ser preferible a la herencia en ciertos casos. Supongamos que estamos trabajando en un sistema de procesamiento de pedidos en una tienda en línea.
- Enfoque con Herencia:
Primero, consideremos un enfoque utilizando herencia para representar diferentes tipos de productos que se pueden comprar, como libros y electrónicos:
// Clase base para productos
class Producto {
String nombre;
double precio;
Producto(String nombre, double precio) {
this.nombre = nombre;
this.precio = precio;
}
void procesarPedido() {
System.out.println("Procesando pedido para " + nombre);
}
}
// Clase para productos electrónicos que hereda de Producto
class ProductoElectronico extends Producto {
String modelo;
ProductoElectronico(String nombre, double precio, String modelo) {
super(nombre, precio);
this.modelo = modelo;
}
}
// Clase para libros que hereda de Producto
class Libro extends Producto {
String autor;
Libro(String nombre, double precio, String autor) {
super(nombre, precio);
this.autor = autor;
}
}
Este enfoque funciona, pero ¿qué pasa si necesitas introducir nuevos tipos de productos o agregar funcionalidades específicas para ciertos tipos de productos?
- Enfoque con Composición:
En lugar de depender completamente de la herencia, podríamos utilizar la composición para manejar diferentes tipos de productos de manera más flexible:
// Clase para productos
class Producto {
String nombre;
double precio;
Producto(String nombre, double precio) {
this.nombre = nombre;
this.precio = precio;
}
void procesarPedido() {
System.out.println("Procesando pedido para " + nombre);
}
}
// Clase para productos electrónicos que utiliza composición
class ProductoElectronico {
Producto producto;
String modelo;
ProductoElectronico(String nombre, double precio, String modelo) {
this.producto = new Producto(nombre, precio);
this.modelo = modelo;
}
// Puedes agregar lógica específica para productos electrónicos si es necesario
void procesarPedidoEspecifico() {
System.out.println("Procesando pedido específico para " + producto.nombre);
}
}
// Clase para libros que utiliza composición
class Libro {
Producto producto;
String autor;
Libro(String nombre, double precio, String autor) {
this.producto = new Producto(nombre, precio);
this.autor = autor;
}
// Puedes agregar lógica específica para libros si es necesario
void procesarPedidoEspecifico() {
System.out.println("Procesando pedido específico para " + producto.nombre);
}
}
En este enfoque, cada tipo de producto tiene una instancia de la clase Producto, lo que permite compartir la lógica común para procesar pedidos. Además, cada tipo de producto puede tener su propia lógica específica mediante métodos como procesarPedidoEspecifico(). Este diseño es más flexible y facilita la introducción de nuevos tipos de productos o la modificación de la lógica específica para cada tipo sin afectar la jerarquía de herencia.
Cuando aplicar herencia?
Si bien la elección entre herencia y composición en el diseño de software depende del contexto y de los requisitos específicos del problema que estás abordando. Aquí hay algunas situaciones en las que podrías considerar la herencia como una opción más apropiada que la composición:
- Relación "es-un": La herencia es especialmente adecuada cuando hay una relación clara de "es-un" entre las clases. Si una clase B es una versión más específica o especializada de una clase A, entonces la herencia tiene sentido. Por ejemplo, si tienes una clase Vehiculo y una clase Automovil, la relación "es-un" es clara, ya que un automóvil es un tipo de vehículo.
class Vehiculo {
// ...
}
class Automovil extends Vehiculo {
// ...
}
- Reutilización de código: La herencia permite la reutilización de código, ya que la clase derivada hereda los miembros y métodos de la clase base. Esto puede ser beneficioso cuando hay una cantidad significativa de código común entre las clases relacionadas.
class Animal {
void comer() {
// Lógica común para comer
}
}
class Perro extends Animal {
void ladrar() {
// Lógica específica para ladrar
}
}
- Polimorfismo: La herencia es fundamental para la implementación del polimorfismo, que permite a las clases derivadas proporcionar implementaciones específicas de los métodos heredados de la clase base. Esto es útil en escenarios donde necesitas tratar objetos de clases derivadas de manera uniforme.
class Figura {
void dibujar() {
// Lógica común para dibujar una figura
}
}
class Circulo extends Figura {
void dibujar() {
// Lógica específica para dibujar un círculo
}
}
class Cuadrado extends Figura {
void dibujar() {
// Lógica específica para dibujar un cuadrado
}
}
- Extensiones de funcionalidad: Si estás extendiendo o especializando funcionalidad existente, la herencia puede ser más natural. Por ejemplo, si estás desarrollando una biblioteca y proporcionas una clase base con una funcionalidad básica, los usuarios de tu biblioteca pueden derivar de esa clase base para extender esa funcionalidad.
Que pasa si hacemos una mala herencia?
Si seguimos evaluando los pros y los contras de la herencia, uno de los problemas que puede surgir de una mala herencia es que violaríamos el Principio de Segregación de Interfaces, que indica que los clientes no deberían verse obligados a depender de interfaces que no utilizan. Si una interfaz se extiende de manera que incluya métodos que no son relevantes para todas las implementaciones, los clientes que utilizan esa interfaz podrían verse forzados a implementar o depender de métodos que no necesitan, lo que puede llevar a un diseño menos limpio y más difícil de mantener.
Conclusión
En resumen, la herencia se centra en la relación "es un" y se utiliza para modelar jerarquías de clases, mientras que la composición se centra en la relación "tiene un" y se utiliza para construir objetos complejos a partir de otros objetos más simples. Ambos enfoques tienen sus casos de uso específicos y se eligen según la estructura y la naturaleza de las relaciones en el diseño del software.
Posted on September 25, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 28, 2024