Herencia y Composición en Java

Herencia y Composición en Java

1. Introducción

Herencia y composición, junto con abstracción, encapsulación y polimorfismo, son fundamentos de la programación orientada a objetos (POO, o en inglés, OOP).

En este tutorial, cubriremos los conceptos básicos de herencia y composición, y nos centraremos en identificar las diferencias entre los dos tipos de relaciones.

2. Conceptos Básicos de la Herencia

La herencia es un mecanismo poderoso pero a menudo sobrecargado y mal utilizado.

En pocas palabras, con la 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 Persona que define los campos y métodos comunes para una persona, mientras que las subclases Camarera y Actriz proporcionan implementaciones adicionales y detalladas de métodos.

Aquí está la clase Persona:

public class Persona {
    private final String nombre;

    // otros campos, constructores estándar, getters

Y estas son las subclases:

public class Camarera extends Persona {

    public String servirEntrada(String tapa) {
        return "Sirviendo una ración de " + tapa;
    }

    // métodos/constructores adicionales
}

public class Actriz extends Persona {

    public String leerGuión(String película) {
        return "Leyendo el guión de " + película;
    }

    // métodos/constructores adicionales

Además, creemos una prueba unitaria para verificar que las instancias de las clases Camarera y Actriz también son instancias de Persona, demostrando así que se cumple la condición "es-un" a nivel de tipo:

@Test
public void dadaInstanciaCamarera_cuandoComprobamosTipo_entoncesEsInstanciaDePersona() {
    assertThat(new Camarera("María", "[email protected]", 24))
        .isInstanceOf(Persona.class);
}

@Test
public void dadaInstanciaActriz_cuandoComprobamosTipo_entoncesEsInstanciaDePersona() {
    assertThat(new Actriz("Cristina", "[email protected]", 35))
        .isInstanceOf(Persona.class);
}

Es importante destacar aquí el aspecto semántico de la herencia. Además de reutilizar la implementación de la clase Persona, hemos creado una relación "es-un" bien definida entre el tipo base Persona y los subtipos Camarera y Actriz. Las camareras y actrices son, efectivamente, personas.

Esto puede llevarnos a preguntarnos: ¿en qué casos es apropiado utilizar la herencia?

Si los subtipos cumplen con la condición "es-un" y principalmente proporcionan funcionalidad aditiva más abajo en la jerarquía de clases, entonces la herencia es el camino a seguir.

Por supuesto, la sobrescritura de métodos está permitida siempre que los métodos sobrescritos preserven la sustituibilidad entre el tipo base y el subtipo 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 cual en algunos casos puede ser excesivo o simplemente indeseable.

De lo contrario, deberíamos usar composición en su lugar.

3. Herencia en Patrones de Diseño

Si bien el consenso es que debemos favorecer la composición sobre la herencia siempre que sea posible, existen algunos casos típicos en los que la herencia tiene su lugar.

3.1 El Patrón de Supertipo de Capa

En este caso, utilizamos la herencia para mover el código común a una clase base (el supertipo).

Aquí hay una implementación básica de este patrón en la capa de dominio:

public class Entidad {
    
    protected long id;
    
    // setters
}

public class Usuario extends Entidad {
    
    // campos y métodos adicionales   
}

Podemos aplicar el mismo enfoque a las otras capas del sistema, como las capas de servicio y persistencia.

3.2 El Patrón de Método de Plantilla

En el patrón de 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 ConstructorDeComputadora {
    
    public final Computadora construirComputadora() {
        agregarProcesador();
        agregarMemoria();
    }
    
    public abstract void agregarProcesador();
    
    public abstract void agregarMemoria();
}

public class ConstructorDeComputadoraEstandar extends ConstructorDeComputadora {

    @Override
    public void agregarProcesador() {
        // implementación del método
    }
    
    @Override
    public void agregarMemoria() {
        // implementación del método
    }
}

4. Conceptos Básicos de la Composición

La composición es otro mecanismo proporcionado por la POO para reutilizar la implementación.

En pocas palabras, la composición nos permite modelar objetos que están formados por otros objetos, definiendo así una relación "tiene-un" 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 por un objeto se destruyen también cuando se destruye ese objeto.

Para comprender mejor cómo funciona la composición, supongamos que necesitamos trabajar con objetos que representan computadoras.

Una computadora está compuesta por diferentes componentes, como el microprocesador, la memoria, una tarjeta de sonido y más, por lo que podemos modelar tanto la computadora como cada uno de sus componentes como clases individuales.

Así es como podría verse una implementación simple de la clase Computadora:

public class Computadora {

    private Procesador procesador;
    private Memoria memoria;
    private TarjetaDeSonido tarjetaDeSonido;

    // getters/setters/constructores estándar
    
    public Optional<TarjetaDeSonido> getTarjetaDeSonido() {
        return Optional.ofNullable(tarjetaDeSonido);
    }
}

Las siguientes clases modelan un microprocesador, la memoria y una tarjeta de sonido (las interfaces se omiten por brevedad):

public class ProcesadorEstandar implements Procesador {

    private String modelo;
    
    // getters/setters estándar
}

public class MemoriaEstandar implements Memoria {
    
    private String marca;
    private String capacidad;
    
    // constructores estándar, getters, toString
}

public class TarjetaDeSonidoEstandar implements TarjetaDeSonido {
    
    private String marca;

    // constructores estándar, getters, toString
}

Es fácil entender las motivaciones detrás de dar prioridad a la composición sobre la herencia. En todos los escenarios en los que es posible establecer una relación semánticamente correcta de "tiene-un" entre una clase dada y otras, la composición es la elección correcta.

En el ejemplo anterior, Computadora cumple con la condición de "tiene-un" con las clases que modelan sus componentes.

También es importante señalar que en este caso, el objeto Computadora tiene la propiedad de los objetos contenidos solo si los objetos no se pueden reutilizar en otra Computadora. Si pueden, estaríamos utilizando agregación en lugar de composición, donde la propiedad no está implícita.

5. Composición Sin Abstracción

Alternativamente, podríamos haber definido la relación de composición codificando las dependencias de la clase Computadora en lugar de declararlas en el constructor:

public class Computadora {

    private ProcesadorEstandar procesador
        = new ProcesadorEstandar("Intel I3");
    private MemoriaEstandar memoria
        = new MemoriaEstandar("Kingston", "1TB");
    
    // campos/métodos adicionales
}

Por supuesto, esto sería un diseño rígido y fuertemente acoplado, ya que haríamos que la clase Computadora dependiera fuertemente 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 con bajo acoplamiento, 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-un" vs. "tiene-un").

-->