La composición sobre herencia (o principio de reutilización compuesta ) en programación orientada a objetos (POO) es el principio según el cual las clases deben favorecer el comportamiento polimórfico y la reutilización de código mediante su composición (al contener instancias de otras clases que implementan la funcionalidad deseada) sobre la herencia de una clase base o principal. [2] Idealmente, toda reutilización se puede lograr mediante el ensamblaje de componentes existentes, pero en la práctica, a menudo se necesita herencia para crear nuevos. Por lo tanto, la herencia y la composición de objetos suelen trabajar de la mano, como se analiza en el libro Design Patterns (1994). [3]
Una implementación de composición sobre herencia generalmente comienza con la creación de varias interfaces que representan los comportamientos que el sistema debe exhibir. Las interfaces pueden facilitar el comportamiento polimórfico . Las clases que implementan las interfaces identificadas se crean y se agregan a las clases del dominio empresarial según sea necesario. De esta manera, los comportamientos del sistema se realizan sin herencia.
De hecho, las clases del dominio empresarial pueden ser todas clases base sin herencia alguna. La implementación alternativa de los comportamientos del sistema se logra proporcionando otra clase que implemente la interfaz de comportamiento deseada. Una clase que contiene una referencia a una interfaz puede admitir implementaciones de la interfaz, una elección que se puede posponer hasta el tiempo de ejecución .
A continuación se muestra un ejemplo en C++ :
clase Objeto { público : virtual void update () { // sin operación } virtual void draw () { // sin operación } vacío virtual colisión ( Objeto objetos []) { // no-op } }; clase Visible : público Objeto { Modelo * modelo ; public : virtual void draw () override { // código para dibujar un modelo en la posición de este objeto } }; clase Solid : público Object { público : virtual void collide ( Object objects []) override { // código para comprobar y reaccionar ante colisiones con otros objetos } }; clase Movable : público Object { público : virtual void update () override { // código para actualizar la posición de este objeto } };
Entonces, supongamos que también tenemos estas clases concretas:
Player
- que es Solid
, Movable
yVisible
Cloud
- que es Movable
y Visible
, pero noSolid
Building
- que es Solid
y Visible
, pero noMovable
Trap
- que es Solid
, pero ni Visible
niMovable
Tenga en cuenta que la herencia múltiple es peligrosa si no se implementa con cuidado porque puede provocar el problema del diamante . Una solución a esto es crear clases como VisibleAndSolid
, VisibleAndMovable
, VisibleAndSolidAndMovable
, etc. para cada combinación necesaria; sin embargo, esto genera una gran cantidad de código repetitivo. C++ utiliza la herencia virtual para resolver el problema del diamante de la herencia múltiple.
Los ejemplos de C++ de esta sección demuestran el principio de utilizar la composición y las interfaces para lograr la reutilización del código y el polimorfismo. Debido a que el lenguaje C++ no tiene una palabra clave dedicada a declarar interfaces, el siguiente ejemplo de C++ utiliza la herencia de una clase base abstracta pura . Para la mayoría de los propósitos, esto es funcionalmente equivalente a las interfaces proporcionadas en otros lenguajes, como Java [4] : 87 y C#. [5] : 144
Introduzca una clase abstracta denominada VisibilityDelegate
, con las subclases NotVisible
y Visible
, que proporciona un medio para dibujar un objeto:
clase VisibilityDelegate { público : virtual void draw () = 0 ; }; clase NotVisible : público VisibilityDelegate { público : virtual void draw () override { // sin operación } }; clase Visible : public VisibilityDelegate { public : virtual void draw () override { // código para dibujar un modelo en la posición de este objeto } };
Introduzca una clase abstracta denominada UpdateDelegate
, con las subclases NotMovable
y Movable
, que proporciona un medio para mover un objeto:
clase UpdateDelegate { público : virtual void update () = 0 ; }; clase NotMovable : público UpdateDelegate { público : virtual void update () override { // no-op } }; clase Movable : public UpdateDelegate { public : virtual void update () override { // código para actualizar la posición de este objeto } };
Introduzca una clase abstracta denominada CollisionDelegate
, con las subclases NotSolid
y Solid
, que proporciona un medio para colisionar con un objeto:
clase CollisionDelegate { público : virtual void collide ( Objeto objetos []) = 0 ; }; clase NotSolid : public CollisionDelegate { public : virtual void collide ( Object objetos []) override { // no-op } }; clase Solid : public CollisionDelegate { public : virtual void collide ( Object objects []) override { // código para verificar y reaccionar ante colisiones con otros objetos } };
Por último, introduzca una clase nombrada Object
con miembros para controlar su visibilidad (usando un VisibilityDelegate
), movilidad (usando un UpdateDelegate
) y solidez (usando un CollisionDelegate
). Esta clase tiene métodos que delegan a sus miembros, por ejemplo, update()
simplemente llama a un método en el UpdateDelegate
:
clase Objeto { VisibilityDelegate * _v ; UpdateDelegate * _u ; CollisionDelegate * _c ; público : Objeto ( VisibilityDelegate * v , UpdateDelegate * u , CollisionDelegate * c ) : _v ( v ) , _u ( u ) , _c ( c ) {} void actualización () { _u -> actualización (); } void dibujar () { _v -> dibujar (); } void colisionar ( Objeto objetos []) { _c -> colisionar ( objetos ); } };
Entonces las clases concretas se verían así:
clase Jugador : Objeto público { público : Jugador () : Objeto ( nuevo Visible (), nuevo Móvil (), nuevo Sólido ()) {} // ... };clase Smoke : objeto público { público : Smoke () : objeto ( nuevo Visible (), nuevo Movable (), nuevo NotSolid ()) {} // ... };
Favorecer la composición sobre la herencia es un principio de diseño que otorga al diseño una mayor flexibilidad. Es más natural construir clases de dominio empresarial a partir de varios componentes que intentar encontrar puntos en común entre ellos y crear un árbol genealógico. Por ejemplo, un pedal de acelerador y un volante comparten muy pocos rasgos comunes , pero ambos son componentes vitales en un automóvil. Lo que pueden hacer y cómo se pueden usar para beneficiar al automóvil se definen fácilmente. La composición también proporciona un dominio empresarial más estable a largo plazo, ya que es menos propenso a las peculiaridades de los miembros de la familia. En otras palabras, es mejor componer lo que un objeto puede hacer ( tiene-un ) que extender lo que es ( es-un ). [1]
El diseño inicial se simplifica al identificar los comportamientos de los objetos del sistema en interfaces separadas en lugar de crear una relación jerárquica para distribuir los comportamientos entre las clases del dominio empresarial mediante la herencia. Este enfoque se adapta más fácilmente a los cambios de requisitos futuros que, de otro modo, requerirían una reestructuración completa de las clases del dominio empresarial en el modelo de herencia. Además, evita los problemas que suelen asociarse con cambios relativamente menores en un modelo basado en herencia que incluye varias generaciones de clases. La relación de composición es más flexible, ya que se puede cambiar en tiempo de ejecución, mientras que las relaciones de subtipificación son estáticas y necesitan recompilación en muchos lenguajes.
Algunos lenguajes, especialmente Go [6] y Rust [7] , utilizan exclusivamente la composición de tipos.
Un inconveniente común de usar la composición en lugar de la herencia es que los métodos proporcionados por componentes individuales pueden tener que implementarse en el tipo derivado, incluso si son solo métodos de reenvío (esto es cierto en la mayoría de los lenguajes de programación, pero no en todos; consulte § Cómo evitar inconvenientes). Por el contrario, la herencia no requiere que todos los métodos de la clase base se vuelvan a implementar dentro de la clase derivada. En cambio, la clase derivada solo necesita implementar (anular) los métodos que tienen un comportamiento diferente al de los métodos de la clase base. Esto puede requerir un esfuerzo de programación significativamente menor si la clase base contiene muchos métodos que proporcionan un comportamiento predeterminado y solo es necesario anular algunos de ellos dentro de la clase derivada.
Por ejemplo, en el código C# que se muestra a continuación, las variables y los métodos de la Employee
clase base son heredados por las HourlyEmployee
subclases SalariedEmployee
derivadas. Solo Pay()
es necesario que cada subclase derivada implemente (especialice) el método. Los demás métodos son implementados por la propia clase base y son compartidos por todas sus subclases derivadas; no es necesario volver a implementarlos (anularlos) ni siquiera mencionarlos en las definiciones de las subclases.
// Clase base clase abstracta pública Empleado { // Propiedades cadena protegida Nombre { obtener ; establecer ; } int protegido ID { obtener ; establecer ; } decimal protegido PayRate { obtener ; establecer ; } int protegido HoursWorked { obtener ; } // Obtener el pago por el período de pago actual public abstract decimal Pay (); } // Subclase derivada public class HourlyEmployee : Employee { // Obtener el pago por el período de pago actual public override decimal Pay () { // El tiempo trabajado está en horas return HoursWorked * PayRate ; } } // Subclase derivada public class SalariedEmployee : Employee { // Obtener el pago por el período de pago actual public override decimal Pay () { // La tasa de pago es el salario anual en lugar de la tasa por hora return HoursWorked * PayRate / 2087 ; } }
Este inconveniente se puede evitar mediante el uso de rasgos , mixins , incrustaciones (de tipo) o extensiones de protocolo .
Algunos idiomas ofrecen medios específicos para mitigar esto:
@Delegate
anotación en el campo, en lugar de copiar y mantener los nombres y tipos de todos los métodos del campo delegado. [14]handles
característica para facilitar el reenvío de métodos. [20]Un estudio de 2013 sobre 93 programas Java de código abierto (de distintos tamaños) descubrió que:
Si bien no existe una gran oportunidad de reemplazar la herencia por la composición (...), la oportunidad es significativa (la mediana del 2% de los usos [de la herencia] son solo reutilización interna, y un 22% adicional son solo reutilización externa o interna). Nuestros resultados sugieren que no hay necesidad de preocuparse por el abuso de la herencia (al menos en el software Java de código abierto), pero sí resaltan la cuestión sobre el uso de la composición frente a la herencia. Si existen costos significativos asociados con el uso de la herencia cuando se podría utilizar la composición, entonces nuestros resultados sugieren que hay algún motivo de preocupación.
— Tempero et al. , "Lo que hacen los programadores con la herencia en Java" [23]