La composición sobre la herencia (o principio de reutilización compuesta ) en la programación orientada a objetos (OOP) es el principio de que las clases deben lograr un comportamiento polimórfico y la reutilización del código por su composición (al contener instancias de otras clases que implementan la funcionalidad deseada) en lugar de la herencia de una clase base o padre. [2] Este es un principio de OOP que se declara a menudo, como en el influyente libro Design Patterns (1994). [3]
Lo esencial
Una implementación de la composición sobre la herencia generalmente comienza con la creación de varias interfaces que representan los comportamientos que debe exhibir el sistema. Las interfaces permiten un comportamiento polimórfico . Las clases que implementan las interfaces identificadas se crean y agregan a las clases de dominio empresarial según sea necesario. Por lo tanto, los comportamientos del sistema se realizan sin herencia.
De hecho, las clases de 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 implementa la interfaz de comportamiento deseada. Una clase que contiene una referencia a una interfaz puede admitir implementaciones de la interfaz, una opción que puede retrasarse hasta el tiempo de ejecución.
Ejemplo
Herencia
A continuación, se muestra un ejemplo en C ++ :
class Object { public : virtual void update () { // no-op } sorteo de vacío virtual () { // no-op } colisión de vacío virtual ( objetos de objeto []) { // no-op } }; clase Visible : objeto público { Modelo * modelo ; public : virtual void draw () override { // código para dibujar un modelo en la posición de este objeto } };class Solid : public Object { public : virtual void collide ( Object objects []) override { // código para comprobar y reaccionar ante colisiones con otros objetos } };class Movable : public Object { public : virtual void update () override { // código para actualizar la posición de este objeto } };
Entonces, supongamos que también tenemos estas clases concretas:
- clase
Player
- que esSolid
,Movable
yVisible
- clase
Cloud
- que esMovable
yVisible
, pero noSolid
- clase
Building
- que esSolid
yVisible
, pero noMovable
- clase
Trap
- que esSolid
, pero niVisible
niMovable
Tenga en cuenta que la herencia múltiple es peligrosa si no se implementa con cuidado, ya que puede conducir al problema del diamante . Una solución para evitar esto es crear clases, tales como VisibleAndSolid
, VisibleAndMovable
, VisibleAndSolidAndMovable
, etc. para cada combinación necesaria, aunque esto lleva a una gran cantidad de código repetitivo. Tenga en cuenta que C ++ resuelve el problema del diamante de la herencia múltiple al permitir la herencia virtual .
Composición e interfaces
Los ejemplos de C ++ de esta sección demuestran el principio de utilizar la composición y las interfaces para lograr la reutilización y el polimorfismo del código. Debido a que el lenguaje C ++ no tiene una palabra clave dedicada para declarar interfaces, el siguiente ejemplo de C ++ usa "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 y C #.
Introduzca una clase abstracta denominada VisibilityDelegate
, con las subclases NotVisible
y Visible
, que proporciona un medio para dibujar un objeto:
clase VisibilityDelegate { public : virtual void draw () = 0 ; };class NotVisible : public VisibilityDelegate { public : virtual void draw () override { // no-op } };class 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 { public : virtual void update () = 0 ; };class NotMovable : public UpdateDelegate { public : virtual void update () override { // no-op } };class 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:
class CollisionDelegate { public : virtual void collide ( Object objects []) = 0 ; };class NotSolid : public CollisionDelegate { public : virtual void collide ( Object objects []) override { // no-op } };class Solid : public CollisionDelegate { public : virtual void collide ( Object objects []) override { // código para verificar y reaccionar ante colisiones con otros objetos } };
Finalmente, introduzca una clase nombrada Object
con miembros para controlar su visibilidad (usando a VisibilityDelegate
), movilidad (usando un UpdateDelegate
) y solidez (usando a CollisionDelegate
). Esta clase tiene métodos que delegan a sus miembros, por ejemplo, update()
simplemente llama a un método en UpdateDelegate
:
class Object { VisibilityDelegate * _v ; UpdateDelegate * _u ; CollisionDelegate * _c ;public : Object ( VisibilityDelegate * v , UpdateDelegate * u , CollisionDelegate * c ) : _v ( v ) , _u ( u ) , _c ( c ) {} actualización vacía () { _u -> actualización (); } vacío dibujar () { _v -> dibujar (); } chocar vacío ( Objetos objetos []) { _c -> chocar ( objetos ); } };
Entonces, las clases concretas se verían así:
class Player : public Object { public : Player () : Object ( new Visible (), new Movable (), new Solid ()) {} // ... };class Smoke : public Object { public : Smoke () : Object ( new Visible (), new Movable (), new NotSolid ()) {} // ... };
Beneficios
Favorecer la composición sobre la herencia es un principio de diseño que le da al diseño una mayor flexibilidad. Es más natural construir clases de dominio empresarial a partir de varios componentes que tratar de 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 en común , pero ambos son componentes vitales en un automóvil. Lo que pueden hacer y cómo pueden usarse para beneficiar al automóvil se define 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 puede hacer un objeto ( HAS-A ) que extender lo que es ( IS-A ). [1]
El diseño inicial se simplifica identificando 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 de dominio de negocios a través de 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 de dominio empresarial en el modelo de herencia. Además, evita problemas asociados a menudo 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 subtipo son estáticas y necesitan recompilarse en muchos idiomas.
Algunos idiomas, en particular Go , utilizan exclusivamente la composición tipográfica. [4]
Inconvenientes
Un inconveniente común de usar composición en lugar de herencia es que los métodos proporcionados por componentes individuales pueden tener que implementarse en el tipo derivado, incluso si solo son métodos de reenvío (esto es cierto en la mayoría de los lenguajes de programación, pero no en todos; consulte 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. Más bien, 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 algunos de ellos deben anularse dentro de la clase derivada.
Por ejemplo, en el código C # siguiente, las variables y métodos de la Employee
clase base son heredados por las subclases HourlyEmployee
y SalariedEmployee
derivadas. Solo el Pay()
método necesita ser implementado (especializado) por cada subclase derivada. Los otros métodos son implementados por la propia clase base y son compartidos por todas sus subclases derivadas; no es necesario volver a implementarlos (anularlos) o incluso mencionarlos en las definiciones de subclase.
// Clase base pública abstracta clase de empleado { // Propiedades protegida cadena Nombre { get ; establecer ; } ID int protegido { get ; establecer ; } PayRate decimal protegido { get ; establecer ; } protegido int HoursWorked { get ; } // Obtener pago por el período de pago actual public abstract decimal Pay (); }// Subclase derivada public class HourlyEmployee : Employee { // Recibe pago por el período de pago actual public override decimal Pay () { // El tiempo trabajado está en horas return HoursWorked * PayRate ; } }// Subclase derivada clase pública SalariedEmployee : Empleado { // Obtener 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 de retorno HoursWorked * PayRate / 2087 ; } }
Evitando inconvenientes
Este inconveniente se puede evitar mediante el uso de rasgos , combinaciones , incrustación (tipo) o extensiones de protocolo .
Algunos idiomas proporcionan medios específicos para mitigar esto:
- C # proporciona métodos de interfaz predeterminados desde la versión 8.0 que permite definir el cuerpo al miembro de la interfaz. [5]
- D proporciona una declaración explícita de "alias this" dentro de un tipo que puede reenviar en él cada método y miembro de otro tipo contenido. [6]
- Dart proporciona mixins con implementaciones predeterminadas que se pueden compartir.
- La incrustación de tipo Go evita la necesidad de métodos de reenvío. [7]
- Java proporciona Project Lombok [8] que permite implementar la delegación usando una sola
@Delegate
anotación en el campo, en lugar de copiar y mantener los nombres y tipos de todos los métodos del campo delegado. [9] Java 8 permite métodos predeterminados en una interfaz, similar a C #, etc. - Las macros de Julia se pueden utilizar para generar métodos de reenvío. Existen varias implementaciones como Lazy.jl y TypedDelegation.jl .
- Kotlin incluye el patrón de delegación en la sintaxis del lenguaje. [10]
- Raku proporciona una
handles
palabra clave para facilitar el reenvío de métodos. - Rust proporciona rasgos con implementaciones predeterminadas.
- Las extensiones Swift se pueden utilizar para definir una implementación predeterminada de un protocolo en el protocolo en sí, en lugar de dentro de la implementación de un tipo individual. [11]
Estudios empíricos
Un estudio de 2013 de 93 programas Java de código abierto (de diferentes tamaños) encontró que:
Si bien no hay una gran [ sic ] oportunidad de reemplazar la herencia con 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 externos o internos reutilizar). 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 pregunta 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 usar la composición, entonces nuestros resultados sugieren que hay algún motivo de preocupación.
- Tempero et al. , "Qué hacen los programadores con la herencia en Java" [12]
Ver también
- Patrón de delegación
- Principio de sustitución de Liskov
- Diseño orientado a objetos
- Programación orientada a roles
- Patrón de estado
- Patrón de estrategia
Referencias
- ^ a b Freeman, Eric; Robson, Elisabeth; Sierra, Kathy; Bates, Bert (2004). Patrones de diseño de Head First . O'Reilly. pag. 23 . ISBN 978-0-596-00712-6.
- ^ Knoernschild, Kirk (2002). Diseño de Java: objetos, UML y proceso: 1.1.5 Principio de reutilización de compuestos (CRP) . Addison-Wesley Inc. ISBN 9780201750447. Consultado el 29 de mayo de 2012 .
- ^ Gamma, Erich ; Helm, Richard; Johnson, Ralph ; Vlissides, John (1994). Patrones de diseño: elementos de software orientado a objetos reutilizable . Addison-Wesley . pag. 20 . ISBN 0-201-63361-2. OCLC 31171684 .
- ^ Pike, Rob (25 de junio de 2012). "Menos es exponencialmente más" . Consultado el 1 de octubre de 2016 .
- ^ "Novedades de C # 8.0" . Microsoft Docs . Microsoft . Consultado el 20 de febrero de 2019 .
- ^ "Alias This" . D Lenguaje de referencia . Consultado el 15 de junio de 2019 .
- ^ " (Tipo) Incrustación" . La documentación del lenguaje de programación Go . Consultado el 10 de mayo de 2019 .
- ^ https://projectlombok.org
- ^ "@Delegar" . Proyecto Lombok . Consultado el 11 de julio de 2018 .
- ^ "Propiedades delegadas" . Referencia de Kotlin . JetBrains . Consultado el 11 de julio de 2018 .
- ^ "Protocolos" . El lenguaje de programación Swift . Apple . Consultado el 11 de julio de 2018 .
- ^ Tempero, Ewan; Yang, Hong Yul; Noble, James (2013). Qué hacen los programadores con la herencia en Java (PDF) . ECOOP 2013 – Programación orientada a objetos. págs. 577–601.