Composición o herencia: Ser o tener

Introducción

El diseño de una solución software basado en objetos requiere que los objetos se relacionen entre sí. Cuando una clase necesita utilizar el código de otra clase necesitamos relacionar estas dos clases. Esta relación la podemos establecer de varias formas y dos de las más importantes en POO son:
  • heredando de una clase base: el objeto adquiere todas las propiedades y comportamientos del objeto base;
  • por composición: conteniendo instancias de otros objetos que implementen esa funcionalidad.
Un principio de diseño en POO dice que ante la posibilidad de resolver un problema con cualquiera de estas dos soluciones deberíamos "favorecer" la composición sobre la herencia. Hay que remarcar que la palabra "favorecer" sólo significa eso: "favorecer". No se trata de una imposición. Más bien es una recomendación cuando ambas posibilidades son aplicables. Existen muchos escenarios en los que nos encontraremos en que las dos soluciones son fácilmente reemplazables entre si y deberemos decidir cual de las dos opciones es más adecuada para cada caso.

Este principio también lo podemos encontrar de forma un poco más extendida: favorecer la composición o las interfaces sobre la herencia. Si es necesario el polimorfismo podemos plantearnos el utilizar interfaces antes que la herencia de clases.

Herencia

La herencia permite centralizar ciertas funcionalidades (reutilización de código) y promueve el polimorfismo (interfaces). Ambas cualidades son dos pilares de la POO pero mal aplicadas pueden resultar en un mal diseño.

El punto donde flaquea la herencia es la rigidez que se produce en la relación entre las dos clases. En el momento en que una clase hereda de otra clase se produce un enlace estático entre la clase y la superclase.

Abuso de la herencia

Cuando aplicamos la herencia con el único propósito de obtener funcionalidad de la base puede ser una señal de que estamos abusando de su uso. Obtener solamente una parte de la funcionalidad de la base nos lleva a romper el principio de sustitución de Liskov, el cual dice que cualquier objeto de una clase debe poder ser sustituido por un objeto de su base.

Para aplicar la herencia debemos tener una relación real entre las clases de tipo ES UN. Un ejemplo típico sería la relación entre un Vehículo representando una abstracción y un Coche o una Moto como clases concretas. De esta forma las subclases se pueden convertir en una especialización de la abstracción.

Si implementamos funcionalidades en la abstracción que sólo tienen sentido en algunas de las implementaciones estaríamos "engordando" la interfaz base con un comportamiento extra para algunas de sus extensiones. Siguiendo con el ejemplo anterior, la implementación de un método en Vehículo que fuera BajarVentanilla tendría sentido en un Coche, Camión, Furgoneta, pero no tendría ningún sentido en una Moto o en una Bicicleta.

Mejor con interfaces...

Para estos casos en los que la herencia de clases no nos sirve pero a su vez queremos utilizar nuestras clases de forma polimórfica, deberíamos utilizar interfaces que implementen funcionalidades específicas de subgrupos. De esta manera, siguiendo el ejemplo de nuestro Vehículo, podríamos tener una interfaz IVentanilla que nos permitiese aplicarla sólo a los vehículos con ventanillas, y una interfaz IIntermitentes sólo a los vehículos con intermitentes. Las interfaces nos permiten una herencia múltiple a través de sus contratos sin implementación de código:



Estos contratos pueden ser incluso implementados por clases totalmente dispares. Tenemos el ejemplo de las interfaces IComparable, IEnumerable, IDisposable, etc.
Estas interfaces permiten a los objetos que las implementan comportarse como tales objetos: polimorfismo. Así, un objeto en el que implementemos la interfaz IComparer nos compromete a tener un método Comparer el cual podrá ser invocado por cualquiera que requiera una comparación entre dos de nuestros objetos, como es el caso del método Sort de la clase ArrayList, el cual aprovecha nuestra cualidad de comparar para realizar su tarea de ordenación. Independientemente de la jerarquía de herencia a la que pertenezca nuestro objeto una cosa es segura, nuestro objeto es comparable, por lo que va a tener este comportamiento disponible para quien así lo requiera.
Nota: las interfaces deben ser simples (interface-segregation principle: ISP). Las clases que las implementan no deberían depender de métodos que no van a usar. Las interfaces grandes deben dividirse en otras más pequeñas y específicas.

Composición

El propósito de la composición es bastante simple de entender: hacer un "todo" formado por sus componentes. Un objeto Coche puede estar compuesto a su vez por un objeto Ruedas, Chasis, Motor, etc. La composición mantiene una relación de pertenencia de tipo TIENE UN.
El punto fuerte de la composición es la flexibilidad que existe en la relación. Podemos usar una abstracción de una clase como parte de otra clase y proporcionar acceso mediante cualquier método de inyección de dependencias. De esta forma podríamos introducir y/o modificar en tiempo de ejecución un objeto concreto y por lo tanto cambiar su comportamiento al vuelo.

En el ejemplo anterior inyectamos una abstracción en el momento en que instanciamos la clase. De esta manera estamos decidiendo en tiempo de ejecución cual será la implementación que usará nuestra clase.

Esta característica es precisamente una consecuencia de haber utilizado la herencia para conseguir depender de una abstracción. Por lo tanto, se puede decir que en la composición se suelen utilizar objetos que a su vez usan la herencia. De hecho, la inyección de dependencias suele emplearse con abstracciones (interfaces o clases abstractas) en lugar de implementaciones concretas. Esta es una forma habitual de composición en sistemas desaónados.

Como contrapartida en la composición, cuando necesitamos exponer funcionalidad de alguno de nuestros componentes nos vemos forzados a implementar esta funcionalidad en el contenedor aunque sólo sea para delegar de forma directa la responsabilidad hacia estos componentes. Esta característica en la herencia se implementa de forma automática ya que la subclase adquiere la interfaz (API) que tiene la superclase.

Aunque en cierto sentido esta contrapartida se podría interpretar también como una ventaja. Puede servir como un arma de encapsulamiento muy potente donde tenemos la facultad de ocultar y/o modificar el modo de acceso a nuestros componentes. Hay patrones de diseño como el Adapter, Decorator o el Wrapper que hacen un buen uso de esta característica.

En el ejemplo estamos envolviendo un objeto Coche en nuestra clase CocheWrapper. El objeto envuelto es inaccesible desde el exterior. Sólo puede acceder a nuestro método público que amplía la funcionalidad del objeto Coche original. Esta tipo de diseño es muy utilizado para desacoplar el cliente que utiliza el Wrapper del objeto envuelto.

Ley de Demeter

Un error en el que podemos caer al utilizar la composición es en las cadenas de llamadas a métodos. Esto ocurre cuando violamos la Ley de Demeter. Para cumplir esta ley debemos tener en cuenta la encapsulación y la cohesión, es decir, restringir el acceso a los miembros de nuestra clase y evitar por otro lado que una clase dependa de clases que no deberían tener una relación directa con la nuestra.
En este ejemplo podemos comprobar que nuestro ejemplo no sólo depende del objeto A, sino que también depende del B, del C e incluso se ha comprometido a usar el método doSomething del objeto C. Esto nos hace vulnerables a cualquier cambio que se produzca en cualquier punto de la cadena. Existe un acoplamiento excesivo entre nuestras clases. Como solución podemos utilizar la técnica del Wrapper vista en el punto anterior.
Otra ventaja de utilizar la composición o las interfaces es la facilidad que da a la hora de refactorizar el código. En la composición no tenemos esa restricción que nos ata de forma directa a una jerarquía donde cualquier cambio aguas arriba afecta a las subclases aguas abajo. En el caso de usar interfaces  lo único que nos ata es nuestro contrato a cada interfaz implementada. Sólo necesitaríamos realizar cambios cuando ese contrato se rompa. La implementación de la interfaz recae en cada clase concreta que suscriba ese contrato.

La composición y la herencia en los patrones de diseño

En el libro Design Patterns [GoF] sus autores hac5n una clasificación de patrones de diseño atendiendo al criterio "alcance" (scope) precisamente haciendo esta distinción:
  • Patrones de clases: usan relaciones entre clases y sus subclases. Se establecen a través de la herencia, así que son fijadas de forma estática en el momento de la compilación;
  • Patrones de objetos: usan relaciones entre objetos mediante la composición. De esta forma se pueden cambiar los objetos de forma dinámica en ejecución.
Hay que resaltar que incluso los patrones de objetos se aprovechan del polimorfismo en sus composiciones. Y sin herencia no hay polimorfismo. Así que se podría decir que los patrones de clases son los que únicamente utilizan las relaciones entre clases (herencia). La mayor parte de ellos son patrones de objetos.

Patrón Estrategia

Un ejemplo es el patrón Estrategia utilizado para cambiar el comportamiento de forma dinámica. Al utilizar la composición para mantener un objeto que encapsula el comportamiento nos facilita la posibilidad de "inyectar" comportamientos distintos en tiempo de ejecución. Si utilizáramos la herencia, este comportamiento sería heredado de la base. Si quisiéramos modificarlo tendríamos que sobrescribir este comportamiento quedando de nuevo estático.

Patrón Decorador

Otro ejemplo es el patrón Decorador en el que envolvemos el objeto a decorar mediante la composición. Antes de delegar la responsabilidad al objeto original podemos "decorar" el comportamiento realizando un preprocesado o postprocesado o ambos. De esta forma estamos añadiendo funcionalidad al objeto decorado sin necesidad de extenderlo.
Written on October 29, 2017