stringtranslate.com

Composición por encima de la herencia

Este diagrama muestra cómo se puede diseñar de manera flexible el comportamiento de vuelo y sonido de un animal utilizando el principio de diseño de composición sobre herencia. [1]

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]

Lo esencial

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 .

Ejemplo

Herencia

A continuación se muestra un ejemplo en C++ :

clase Objeto { público : virtual void update () { // sin operación }        void virtual 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 : public Object { public : 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:

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.

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 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 NotVisibley 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 NotMovabley 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 NotSolidy 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 Objectcon 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 ()) {}              // ... };

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 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.

Desventajas

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 Employeeclase base son heredados por las HourlyEmployeesubclases SalariedEmployeederivadas. 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 UML Employee.svg

// 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 ; } }                  

Cómo evitar inconvenientes

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:

Estudios empíricos

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]

Véase también

Referencias

  1. ^ ab Freeman, Eric; Robson, Elisabeth; Sierra, Kathy; Bates, Bert (2004). Patrones de diseño Head First . O'Reilly. pág. 23. ISBN 978-0-596-00712-6.
  2. ^ Knoernschild, Kirk (2002). Diseño de Java: objetos, UML y procesos: 1.1.5 Principio de reutilización compuesta (CRP). Addison-Wesley Inc. ISBN 9780201750447. Recuperado el 29 de mayo de 2012 .
  3. ^ Gamma, Erich ; Helm, Richard; Johnson, Ralph ; Vlissides, John (1994). Patrones de diseño: elementos de software orientado a objetos reutilizable . Addison-Wesley . pág. 20. ISBN. 0-201-63361-2.OCLC 31171684  .
  4. ^ ab Bloch, Joshua (2018). "Effective Java: Programming Language Guide" (tercera edición). Addison-Wesley. ISBN 978-0134685991.
  5. ^ ab Price, Mark J. (2022). C# 8.0 y .NET Core 3.0: desarrollo multiplataforma moderno: cree aplicaciones con C#, .NET Core, Entity Framework Core, ASP.NET Core y ML.NET con Visual Studio Code . ISBN 978-1-098-12195-2.
  6. ^ Pike, Rob (25 de junio de 2012). "Menos es exponencialmente más" . Consultado el 1 de octubre de 2016 .
  7. ^ "Características de los lenguajes orientados a objetos: el lenguaje de programación Rust". doc.rust-lang.org . Consultado el 10 de octubre de 2022 .
  8. ^ "Novedades de C# 8.0". Microsoft Docs . Microsoft . Consultado el 20 de febrero de 2019 .
  9. ^ Skeet, Jon (23 de marzo de 2019). C# en profundidad . Manning. ISBN 978-1617294532.
  10. ^ Albahari, José (2022). C# 10 en pocas palabras . O'Reilly. ISBN 978-1-098-12195-2.
  11. ^ "Alias ​​This". Referencia del lenguaje D. Consultado el 15 de junio de 2019 .
  12. ^ "(Tipo) Incrustación". Documentación del lenguaje de programación Go . Consultado el 10 de mayo de 2019 .
  13. ^ https://projectlombok.org [ URL básica ]
  14. ^ "@Delegate". Proyecto Lombok . Consultado el 11 de julio de 2018 .
  15. ^ "MikeInnes/Lazy.jl". GitHub .
  16. ^ "JeffreySarnoff/TypedDelegation.jl". GitHub .
  17. ^ "Macro de reenvío de método". JuliaLang . 20 de abril de 2019 . Consultado el 18 de agosto de 2022 .
  18. ^ "Propiedades delegadas". Referencia de Kotlin . JetBrains . Consultado el 11 de julio de 2018 .
  19. ^ "PHP: Traits" (Rasgos de PHP). www.php.net . Consultado el 23 de febrero de 2023 .
  20. ^ "Sistema de tipos". docs.raku.org . Consultado el 18 de agosto de 2022 .
  21. ^ "Cláusulas de exportación". Documentación de Scala . Consultado el 6 de octubre de 2021 .
  22. ^ "Protocolos". El lenguaje de programación Swift . Apple Inc. Consultado el 11 de julio de 2018 .
  23. ^ Tempero, Ewan; Yang, Hong Yul; Noble, James (2013). "Lo que hacen los programadores con la herencia en Java" (PDF) . ECOOP 2013 – Programación orientada a objetos . ECOOP 2013–Programación orientada a objetos. Apuntes de clase en informática. Vol. 7920. págs. 577–601. doi :10.1007/978-3-642-39038-8_24. ISBN 978-3-642-39038-8.