1. Información general
La herencia y la composición, junto con la abstracción, la encapsulación y el polimorfismo, son piedras angulares de la programación orientada a objetos (OOP).
En este tutorial, cubriremos los conceptos básicos de herencia y composición, y nos enfocaremos fuertemente en detectar las diferencias entre los dos tipos de relaciones.
2. Conceptos básicos de la herencia
La herencia es un mecanismo poderoso pero abusado y mal utilizado.
En pocas palabras, con herencia, una clase base (también conocida como tipo base) define el estado y el comportamiento común para un tipo dado y permite que las subclases (también conocidas como subtipos) proporcionen versiones especializadas de ese estado y comportamiento.
Para tener una idea clara de cómo trabajar con la herencia, creemos un ejemplo ingenuo: una clase base Person que define los campos y métodos comunes para una persona, mientras que las subclases Waitress y Actress proporcionan implementaciones de métodos adicionales y detalladas.
Aquí está la clase Person :
public class Person { private final String name; // other fields, standard constructors, getters }
Y estas son las subclases:
public class Waitress extends Person { public String serveStarter(String starter) { return "Serving a " + starter; } // additional methods/constructors }
public class Actress extends Person { public String readScript(String movie) { return "Reading the script of " + movie; } // additional methods/constructors }
Además, creemos una prueba unitaria para verificar que las instancias de las clases Waitress y Actress también sean instancias de Person , mostrando así que la condición "es-a" se cumple a nivel de tipo:
@Test public void givenWaitressInstance_whenCheckedType_thenIsInstanceOfPerson() { assertThat(new Waitress("Mary", "[email protected]", 22)) .isInstanceOf(Person.class); } @Test public void givenActressInstance_whenCheckedType_thenIsInstanceOfPerson() { assertThat(new Actress("Susan", "[email protected]", 30)) .isInstanceOf(Person.class); }
Es importante destacar aquí la faceta semántica de la herencia . Además de reutilizar la implementación de la clase Person , hemos creado una relación "es-a" bien definida entre el tipo base Person y los subtipos Waitress y Actress . Las camareras y las actrices son, efectivamente, personas.
Esto puede hacer que nos preguntemos: ¿ en qué casos de uso es la herencia el enfoque correcto a seguir?
Si los subtipos cumplen la condición "es-a" y principalmente proporcionan funcionalidad aditiva más abajo en la jerarquía de clases, la herencia es el camino a seguir.
Por supuesto, se permite la sustitución de métodos siempre que los métodos sustituidos conserven la sustituibilidad de tipo / subtipo base promovida por el principio de sustitución de Liskov.
Además, debemos tener en cuenta que los subtipos heredan la API del tipo base , lo que en algunos casos puede ser excesivo o simplemente indeseable.
De lo contrario, deberíamos usar composición en su lugar.
3. Herencia en los patrones de diseño
Si bien el consenso es que deberíamos favorecer la composición sobre la herencia siempre que sea posible, existen algunos casos de uso típicos en los que la herencia tiene su lugar.
3.1. El patrón de supertipo de capa
En este caso, usamos la herencia para mover código común a una clase base (el supertipo), por capa .
Aquí hay una implementación básica de este patrón en la capa de dominio:
public class Entity { protected long id; // setters }
public class User extends Entity { // additional fields and methods }
Podemos aplicar el mismo enfoque a las otras capas del sistema, como las capas de servicio y persistencia.
3.2. El patrón del método de plantilla
En el patrón del método de plantilla, podemos usar una clase base para definir las partes invariantes de un algoritmo y luego implementar las partes variantes en las subclases :
public abstract class ComputerBuilder { public final Computer buildComputer() { addProcessor(); addMemory(); } public abstract void addProcessor(); public abstract void addMemory(); }
public class StandardComputerBuilder extends ComputerBuilder { @Override public void addProcessor() { // method implementation } @Override public void addMemory() { // method implementation } }
4. Conceptos básicos de la composición
La composición es otro mecanismo proporcionado por OOP para reutilizar la implementación.
En pocas palabras, la composición nos permite modelar objetos que se componen de otros objetos , definiendo así una relación "tiene-a" entre ellos.
Además, la composición es la forma más fuerte de asociación , lo que significa que los objetos que componen o están contenidos en un objeto también se destruyen cuando ese objeto se destruye .
Para comprender mejor cómo funciona la composición, supongamos que necesitamos trabajar con objetos que representan computadoras .
Una computadora se compone de diferentes partes, incluido el microprocesador, la memoria, una tarjeta de sonido, etc., por lo que podemos modelar tanto la computadora como cada una de sus partes como clases individuales.
Así es como se vería una implementación simple de la clase Computer :
public class Computer { private Processor processor; private Memory memory; private SoundCard soundCard; // standard getters/setters/constructors public Optional getSoundCard() { return Optional.ofNullable(soundCard); } }
Las siguientes clases modelan un microprocesador, la memoria y una tarjeta de sonido (las interfaces se omiten en aras de la brevedad):
public class StandardProcessor implements Processor { private String model; // standard getters/setters }
public class StandardMemory implements Memory { private String brand; private String size; // standard constructors, getters, toString }
public class StandardSoundCard implements SoundCard { private String brand; // standard constructors, getters, toString }
Es fácil comprender las motivaciones detrás de empujar la composición sobre la herencia. En todos los escenarios en los que es posible establecer una relación "tiene-a" semánticamente correcta entre una clase determinada y otras, la composición es la elección correcta.
In the above example, Computer meets the “has-a” condition with the classes that model its parts.
It's also worth noting that in this case, the containing Computer object has ownership of the contained objects if and only if the objects can't be reused within another Computer object. If they can, we'd be using aggregation, rather than composition, where ownership isn't implied.
5. Composition Without Abstraction
Alternatively, we could've defined the composition relationship by hard-coding the dependencies of the Computer class, instead of declaring them in the constructor:
public class Computer { private StandardProcessor processor = new StandardProcessor("Intel I3"); private StandardMemory memory = new StandardMemory("Kingston", "1TB"); // additional fields / methods }
Por supuesto, este sería un diseño rígido y estrechamente acoplado, ya que haríamos que la computadora dependiera en gran medida de implementaciones específicas de procesador y memoria .
No estaríamos aprovechando el nivel de abstracción proporcionado por las interfaces y la inyección de dependencias.
Con el diseño inicial basado en interfaces, obtenemos un diseño poco acoplado, que también es más fácil de probar.
6. Conclusión
En este artículo, aprendimos los fundamentos de la herencia y la composición en Java, y exploramos en profundidad las diferencias entre los dos tipos de relaciones (“es-a” vs. “tiene-a”).
Como siempre, todos los ejemplos de código que se muestran en este tutorial están disponibles en GitHub.