¿Prefieres la composición sobre la herencia?

1604

¿Por qué preferir la composición sobre la herencia? ¿Qué compensaciones hay para cada enfoque? ¿Cuándo debería elegir la herencia sobre la composición?

Solo lectura
fuente
3
Vea también qué diseño de clase es mejor
maccullt
40
Hay un buen artículo sobre esa pregunta aquí . Mi opinión personal es que no hay un principio "mejor" o "peor" para diseñar. Hay un diseño "apropiado" e "inadecuado" para la tarea concreta. En otras palabras: uso la herencia o la composición, según la situación. El objetivo es producir código más pequeño, más fácil de leer, reutilizar y, finalmente, ampliar aún más.
m_pGladiator
1
en una oración, la herencia es pública si tiene un método público y lo cambia, cambia la API publicada. Si tiene composición y el objeto compuesto ha cambiado, no tiene que cambiar su API publicada.
Tomer Ben David

Respuestas:

1189

Prefiera la composición sobre la herencia, ya que es más maleable / fácil de modificar más adelante, pero no utilice un enfoque de componer siempre. Con la composición, es fácil cambiar el comportamiento sobre la marcha con Dependency Injection / Setters. La herencia es más rígida ya que la mayoría de los idiomas no le permiten derivar de más de un tipo. Entonces, el ganso se cocina más o menos una vez que deriva del Tipo A.

Mi prueba de ácido para lo anterior es:

  • ¿Quiere TypeB exponer la interfaz completa (todos los métodos públicos no menos) de TypeA de modo que TypeB pueda usarse donde se espera TypeA? Indica herencia .

    • Por ejemplo, un biplano Cessna expondrá la interfaz completa de un avión, si no más. Eso hace que sea apropiado derivar del avión.
  • ¿TypeB desea solo parte / parte del comportamiento expuesto por TypeA? Indica necesidad de composición.

    • Por ejemplo, un pájaro puede necesitar solo el comportamiento de vuelo de un avión. En este caso, tiene sentido extraerlo como una interfaz / clase / ambos y convertirlo en miembro de ambas clases.

Actualización: Acabo de regresar a mi respuesta y parece que ahora está incompleta sin una mención específica del Principio de sustitución de Liskov de Barbara Liskov como prueba para '¿Debería heredar de este tipo?'

Gishu
fuente
81
El segundo ejemplo está sacado directamente del libro Head First Design Patterns ( amazon.com/First-Design-Patterns-Elisabeth-Freeman/dp/… ) :) Recomiendo encarecidamente ese libro a cualquiera que haya buscado en Google esta pregunta.
Jeshurun
44
Está muy claro, pero puede faltar algo: "¿TipoB quiere exponer la interfaz completa (todos los métodos públicos no menos) de TipoA de modo que se pueda usar TipoB donde se espera TipoA?" Pero, ¿qué pasa si esto es cierto y TypeB también expone la interfaz completa de TypeC? ¿Y qué pasa si TypeC aún no se ha modelado?
Tristan
44
Aludir a lo que creo que debería ser la prueba más básica: "¿Debería este objeto ser utilizable por código que espera objetos de (lo que sería) el tipo base". Si la respuesta es sí, el objeto debe heredar. Si no, entonces probablemente no debería. Si tuviera mis habilidades, los idiomas proporcionarían una palabra clave para referirse a "esta clase", y proporcionarían un medio para definir una clase que debería comportarse como otra clase, pero no ser sustituible por ella (tal clase tendría todo "esto clase "referencias reemplazadas por sí mismo).
supercat
22
@Alexey: el punto es "¿Puedo pasar un biplano Cessna a todos los clientes que esperan un avión sin sorprenderlos?". En caso afirmativo, entonces es probable que desee herencia.
Gishu
99
De hecho, me cuesta pensar en ejemplos en los que la herencia hubiera sido mi respuesta, a menudo encuentro que la agregación, la composición y las interfaces dan como resultado soluciones más elegantes. Muchos de los ejemplos anteriores podrían explicarse mejor utilizando esos enfoques ...
Stuart Wakefield
415

Piense en la contención como en una relación. Un automóvil "tiene un" motor, una persona "tiene un" nombre, etc.

Piense en la herencia como una es una relación. Un automóvil "es un" vehículo, una persona "es un" mamífero, etc.

No me atribuyo crédito por este enfoque. Lo tomé directamente de la Segunda Edición de Code Complete de Steve McConnell , Sección 6.3 .

Nick Zalutskiy
fuente
107
Este no es siempre un enfoque perfecto, es simplemente una buena guía. El principio de sustitución de Liskov es mucho más preciso (falla menos).
Bill K
41
"Mi auto tiene un vehículo". Si considera eso como una oración separada, no en un contexto de programación, eso no tiene absolutamente ningún sentido. Y ese es el objetivo de esta técnica. Si suena incómodo, probablemente esté mal.
Nick Zalutskiy
36
@Nick Claro, pero "Mi auto tiene un comportamiento del vehículo" tiene más sentido (supongo que su clase "Vehículo" podría llamarse "Comportamiento del vehículo"). Por lo tanto, no puede basar su decisión en "tiene un" vs "es una" comparación, tiene que usar LSP, o cometerá errores
Tristan
35
En lugar de "es un" pensar en "se comporta como". La herencia se trata de heredar el comportamiento, no solo la semántica.
ybakos
44
@ybakos "Behaves like" se puede lograr a través de interfaces sin necesidad de herencia. De Wikipedia : "Una implementación de la composición sobre la herencia generalmente comienza con la creación de varias interfaces que representan los comportamientos que el sistema debe exhibir ... Por lo tanto, los comportamientos del sistema se realizan sin herencia".
DavidRR
210

Si comprende la diferencia, es más fácil de explicar.

Código de procedimiento

Un ejemplo de esto es PHP sin el uso de clases (particularmente antes de PHP5). Toda la lógica está codificada en un conjunto de funciones. Puede incluir otros archivos que contengan funciones auxiliares, etc., y llevar a cabo su lógica empresarial pasando datos en funciones. Esto puede ser muy difícil de administrar a medida que crece la aplicación. PHP5 intenta remediar esto ofreciendo un diseño más orientado a objetos.

Herencia

Esto fomenta el uso de clases. La herencia es uno de los tres principios del diseño OO (herencia, polimorfismo, encapsulación).

class Person {
   String Title;
   String Name;
   Int Age
}

class Employee : Person {
   Int Salary;
   String Title;
}

Esto es herencia en el trabajo. El empleado "es una" persona o hereda de la persona. Todas las relaciones de herencia son relaciones "es-a". El Empleado también sombrea la propiedad Título de Persona, lo que significa Empleado. El Título devolverá el Título para el Empleado, no para la Persona.

Composición

Se favorece la composición sobre la herencia. En pocas palabras, tendrías:

class Person {
   String Title;
   String Name;
   Int Age;

   public Person(String title, String name, String age) {
      this.Title = title;
      this.Name = name;
      this.Age = age;
   }

}

class Employee {
   Int Salary;
   private Person person;

   public Employee(Person p, Int salary) {
       this.person = p;
       this.Salary = salary;
   }
}

Person johnny = new Person ("Mr.", "John", 25);
Employee john = new Employee (johnny, 50000);

La composición es típicamente "tiene una" o "usa una" relación. Aquí la clase Empleado tiene una Persona. No hereda de Person, sino que le pasa el objeto Person, por lo que "tiene una" Person.

Composición sobre herencia

Ahora digamos que desea crear un tipo de Administrador para que termine con:

class Manager : Person, Employee {
   ...
}

Este ejemplo funcionará bien, sin embargo, ¿qué pasa si la Persona y el Empleado declararon Title? ¿Debería Manager.Title devolver "Manager of Operations" o "Mr."? Bajo composición, esta ambigüedad se maneja mejor:

Class Manager {
   public string Title;
   public Manager(Person p, Employee e)
   {
      this.Title = e.Title;
   }
}

El objeto Administrador se compone como Empleado y Persona. El comportamiento del título se toma del empleado. Esta composición explícita elimina la ambigüedad, entre otras cosas, y encontrará menos errores.

aleemb
fuente
66
Para la herencia: no hay ambigüedad. Está implementando la clase Manager según los requisitos. Por lo tanto, devolvería el "Administrador de operaciones" si eso es lo que especificaron sus requisitos, de lo contrario, simplemente usaría la implementación de la clase base. También podría convertir a Person en una clase abstracta y, por lo tanto, asegurarse de que las clases posteriores implementen una propiedad Title.
Raj Rao
68
Es importante recordar que uno podría decir "Composición sobre herencia", pero eso no significa "Composición siempre sobre Herencia". "Es un" significa herencia y conduce a la reutilización del código. El empleado es una persona (el empleado no tiene una persona).
Raj Rao
36
El ejemplo es confuso: el empleado es una persona, por lo que debe usar la herencia. No debe usar composición para este ejemplo, porque es una relación incorrecta en el modelo de dominio, incluso si técnicamente puede declararlo en el código.
Michael Freidgeim
15
No estoy de acuerdo con este ejemplo. Un empleado es una persona, que es un caso de libro de texto de uso adecuado de la herencia. También creo que el "problema" de la redefinición del campo Título no tiene sentido. El hecho de que Employee.Title sombree Person.Title es un signo de mala programación. Después de todo, son "Sr." y "Gerente de Operaciones", ¿realmente se refiere al mismo aspecto de una persona (en minúsculas)? Cambiaría el nombre de Employee.Title, y así podría hacer referencia a los atributos Title y JobTitle de un Employee, los cuales tienen sentido en la vida real. Además, no hay razón para Gerente (continuación ...)
Radon Rosborough
99
(... continuación) heredar de la Persona y el Empleado - después de todo, el Empleado ya hereda de la Persona. En modelos más complejos, donde una persona podría ser un Gerente y un Agente, es cierto que se puede usar la herencia múltiple (¡con cuidado!), Pero sería preferible en muchos entornos tener una clase de Rol abstracta de la que el Gerente (contenga Empleados él / ella administra) y el Agente (contiene Contratos y otra información) heredan. Entonces, un empleado es una persona que tiene múltiples roles. Por lo tanto, tanto la composición como la herencia se usan correctamente.
Radon Rosborough
142

Con todos los beneficios innegables proporcionados por la herencia, estas son algunas de sus desventajas.

Desventajas de la herencia:

  1. No puede cambiar la implementación heredada de las superclases en tiempo de ejecución (obviamente porque la herencia se define en tiempo de compilación).
  2. La herencia expone una subclase a los detalles de su implementación de clase primaria, por eso a menudo se dice que la herencia rompe la encapsulación (en un sentido que realmente necesita enfocarse en las interfaces, no solo en la implementación, por lo que no siempre es preferible reutilizar por subclasificación).
  3. El acoplamiento estrecho proporcionado por la herencia hace que la implementación de una subclase esté muy ligada a la implementación de una superclase que cualquier cambio en la implementación primaria obligará a la subclase a cambiar.
  4. La reutilización excesiva por subclasificación puede hacer que la pila de herencia sea muy profunda y también muy confusa.

Por otro lado, la composición de objetos se define en tiempo de ejecución a través de objetos que adquieren referencias a otros objetos. En tal caso, estos objetos nunca podrán alcanzar los datos protegidos de los demás (sin interrupción de encapsulación) y se verán obligados a respetar la interfaz de los demás. Y también en este caso, las dependencias de implementación serán mucho menores que en caso de herencia.

Galilyou
fuente
55
Esta es una de las mejores respuestas, en mi opinión: agregaré a esto que tratar de repensar sus problemas en términos de composición, en mi experiencia, tiende a conducir a clases más pequeñas, más simples, más independientes y más reutilizables. , con un alcance de responsabilidad más claro, más pequeño y más enfocado. A menudo, esto significa que hay menos necesidad de cosas como la inyección de dependencia o la burla (en las pruebas) ya que los componentes más pequeños generalmente pueden sostenerse por sí mismos. Solo mi experiencia. YMMV :-)
mindplay.dk
3
El último párrafo de esta publicación realmente hizo clic para mí. Gracias.
Salx
87

Otra razón muy pragmática para preferir la composición a la herencia tiene que ver con su modelo de dominio y asignarlo a una base de datos relacional. Es muy difícil asignar la herencia al modelo SQL (terminas con todo tipo de soluciones alternativas, como crear columnas que no siempre se usan, usar vistas, etc.). Algunos ORML intentan lidiar con esto, pero siempre se complica rápidamente. La composición se puede modelar fácilmente a través de una relación de clave externa entre dos tablas, pero la herencia es mucho más difícil.

Tim Howland
fuente
81

Si bien, en pocas palabras, estaría de acuerdo con "Prefiero la composición sobre la herencia", muy a menudo para mí suena como "prefiero las papas a la coca-cola". Hay lugares para la herencia y lugares para la composición. Necesitas entender la diferencia, entonces esta pregunta desaparecerá. Lo que realmente significa para mí es "si vas a usar la herencia, piénsalo de nuevo, es probable que necesites composición".

Debes preferir las papas a la coca cola cuando quieras comer, y la coca cola a las papas cuando quieras beber.

Crear una subclase debería significar más que una forma conveniente de llamar a métodos de superclase. Debería usar la herencia cuando la subclase "es-a" superclase tanto estructural como funcionalmente, cuando puede usarse como superclase y va a usar eso. Si no es el caso, no es herencia, sino algo más. La composición es cuando tus objetos consisten en otro, o tienen alguna relación con ellos.

Entonces, para mí, parece que si alguien no sabe si necesita herencia o composición, el verdadero problema es que no sabe si quiere beber o comer. Piensa más en tu dominio del problema, entiéndelo mejor.

Pavel Feldman
fuente
55
La herramienta adecuada para el trabajo correcto. Un martillo puede ser mejor para golpear cosas que una llave inglesa, pero eso no significa que uno deba ver una llave inglesa como "un martillo inferior". La herencia puede ser útil cuando las cosas que se agregan a la subclase son necesarias para que el objeto se comporte como un objeto de superclase. Por ejemplo, considere una clase base InternalCombustionEnginecon una clase derivada GasolineEngine. Este último agrega cosas como bujías, de las que carece la clase base, pero usarlas como una InternalCombustionEnginecausa hará que las bujías se usen.
supercat
61

La herencia es bastante atractiva, especialmente si proviene de tierras procesales y, a menudo, se ve engañosamente elegante. Quiero decir, todo lo que necesito hacer es agregar esta funcionalidad a alguna otra clase, ¿verdad? Bueno, uno de los problemas es que

La herencia es probablemente la peor forma de acoplamiento que puede tener

Su clase base rompe la encapsulación al exponer los detalles de implementación a subclases en forma de miembros protegidos. Esto hace que su sistema sea rígido y frágil. Sin embargo, la falla más trágica es que la nueva subclase trae consigo todo el equipaje y la opinión de la cadena de herencia.

El artículo, Inheritance is Evil: The Epic Fail of the DataAnnotationsModelBinder , muestra un ejemplo de esto en C #. Muestra el uso de la herencia cuando la composición debería haber sido utilizada y cómo podría ser refactorizada.

Mike Valenty
fuente
La herencia no es buena ni mala, es simplemente un caso especial de Composición. Donde, de hecho, la subclase está implementando una funcionalidad similar a la superclase. Si su subclase propuesta no se está volviendo a implementar sino que simplemente está utilizando la funcionalidad de la superclase, entonces ha utilizado Herencia incorrectamente. Ese es el error del programador, no una reflexión sobre la herencia.
iPherian
42

En Java o C #, un objeto no puede cambiar su tipo una vez que se ha instanciado.

Por lo tanto, si su objeto necesita aparecer como un objeto diferente o comportarse de manera diferente según el estado o las condiciones de un objeto, use Composición : consulte Estado y estrategia Patrones de diseño de .

Si el objeto necesita ser del mismo tipo, use Herencia o implemente interfaces.

dance2die
fuente
10
+1 He encontrado cada vez menos que la herencia funciona en la mayoría de las situaciones. Prefiero las interfaces compartidas / heredadas y la composición de los objetos ... ¿o se llama agregación? ¡No me preguntes, tengo un título de EE!
kenny
Creo que este es el escenario más común donde se aplica la "composición sobre la herencia", ya que ambos podrían ser adecuados en teoría. Por ejemplo, en un sistema de marketing puede tener el concepto de a Client. Entonces, un nuevo concepto de PreferredClientemergente aparece más adelante. Debe PreferredClientheredar Client? Un cliente preferido 'es un' cliente después de todo, ¿no? Bueno, no tan rápido ... como dijiste que los objetos no pueden cambiar su clase en tiempo de ejecución. ¿Cómo modelarías la client.makePreferred()operación? ¿Quizás la respuesta está en usar la composición con un concepto perdido, un Accountquizás?
plalx
En lugar de tener diferentes tipos de Clientclases, tal vez solo haya una que encapsule el concepto de una Accountque podría ser una StandardAccounto una PreferredAccount...
plalx
40

No encontré una respuesta satisfactoria aquí, así que escribí una nueva.

Para entender por qué " preferir la composición sobre la herencia", primero necesitamos recuperar la suposición omitida en este lenguaje abreviado.

Hay dos beneficios de la herencia: subtipificación y subclasificación.

  1. Subtipar significa ajustarse a una firma de tipo (interfaz), es decir, un conjunto de API, y uno puede anular parte de la firma para lograr el polimorfismo de subtipificación.

  2. Subclasificar significa reutilización implícita de implementaciones de métodos.

Con los dos beneficios vienen dos propósitos diferentes para hacer la herencia: orientado a subtipos y orientado a reutilización de código.

Si la reutilización del código es el único propósito, la subclase puede dar uno más de lo que necesita, es decir, algunos métodos públicos de la clase principal no tienen mucho sentido para la clase secundaria. En este caso, en lugar de favorecer la composición sobre la herencia, se exige la composición . Aquí es también de donde proviene la noción "es-a" vs. "tiene-a".

Entonces, solo cuando se propone el subtipo, es decir, usar la nueva clase más tarde de manera polimórfica, nos enfrentamos al problema de elegir la herencia o la composición. Esta es la suposición que se omite en el lenguaje abreviado en discusión.

Subtipo es conformarse a una firma de tipo, esto significa que la composición siempre debe exponer no menos cantidad de API del tipo. Ahora comienzan las compensaciones:

  1. La herencia proporciona la reutilización de código directo si no se anula, mientras que la composición tiene que volver a codificar cada API, incluso si es solo un simple trabajo de delegación.

  2. La herencia proporciona una recursión abierta directa a través del sitio polimórfico interno this, es decir, invocando el método de anulación (o incluso el tipo ) en otra función miembro, ya sea pública o privada (aunque desaconsejada ). La recursión abierta se puede simular a través de la composición , pero requiere un esfuerzo adicional y no siempre es viable (?). Esta respuesta a una pregunta duplicada habla algo similar.

  3. La herencia expone a los miembros protegidos . Esto interrumpe la encapsulación de la clase padre y, si se usa en una subclase, se introduce otra dependencia entre el niño y su padre.

  4. La composición tiene el beneficio de la inversión de control, y su dependencia puede inyectarse dinámicamente, como se muestra en el patrón decorador y el patrón proxy .

  5. La composición tiene el beneficio de la programación orientada al combinador , es decir, funciona de manera similar al patrón compuesto .

  6. La composición sigue inmediatamente a la programación a una interfaz .

  7. La composición tiene el beneficio de una herencia múltiple fácil .

Con las compensaciones anteriores en mente, por lo tanto, preferimos la composición sobre la herencia. Sin embargo, para clases estrechamente relacionadas, es decir, cuando la reutilización implícita de código realmente genera beneficios, o si se desea el poder mágico de la recursión abierta, la herencia será la elección.

lcn
fuente
34

Personalmente aprendí a preferir siempre la composición sobre la herencia. No hay problema programático que pueda resolver con herencia que no pueda resolver con composición; aunque es posible que deba utilizar interfaces (Java) o protocolos (Obj-C) en algunos casos. Dado que C ++ no sabe tal cosa, tendrá que usar clases base abstractas, lo que significa que no puede deshacerse por completo de la herencia en C ++.

La composición a menudo es más lógica, proporciona una mejor abstracción, una mejor encapsulación, una mejor reutilización del código (especialmente en proyectos muy grandes) y es menos probable que rompa algo a distancia solo porque realizó un cambio aislado en cualquier parte de su código. También facilita la defensa del " Principio de responsabilidad única ", que a menudo se resume como " Nunca debería haber más de una razón para que una clase cambie ", y significa que cada clase existe para un propósito específico y debe solo tiene métodos que están directamente relacionados con su propósito. Además, tener un árbol de herencia muy poco profundo hace que sea mucho más fácil mantener la visión general incluso cuando su proyecto comienza a ser realmente grande. Mucha gente piensa que la herencia representa nuestro mundo realbastante bien, pero esa no es la verdad. El mundo real usa mucha más composición que herencia. Casi todos los objetos del mundo real que puedes sostener en tu mano se han compuesto de otros objetos más pequeños del mundo real.

Sin embargo, hay desventajas en la composición. Si omite la herencia por completo y solo se enfoca en la composición, notará que a menudo tiene que escribir un par de líneas de código adicionales que no serían necesarias si hubiera utilizado la herencia. A veces también se ve obligado a repetirlo y esto viola el Principio DRY(SECO = No te repitas). Además, la composición a menudo requiere delegación, y un método simplemente está llamando a otro método de otro objeto sin otro código que rodea esta llamada. Tales "llamadas de método doble" (que pueden extenderse fácilmente a llamadas de método triple o cuádruple e incluso más allá de eso) tienen un rendimiento mucho peor que la herencia, donde simplemente hereda un método de sus padres. Llamar a un método heredado puede ser igual de rápido que llamar a uno no heredado, o puede ser un poco más lento, pero generalmente sigue siendo más rápido que dos llamadas a métodos consecutivos.

Es posible que haya notado que la mayoría de los lenguajes OO no permiten la herencia múltiple. Si bien hay un par de casos en los que la herencia múltiple realmente puede comprarle algo, esas son más bien excepciones que la regla. Siempre que se encuentre con una situación en la que piense que "la herencia múltiple sería una característica realmente genial para resolver este problema", generalmente se encuentra en un punto en el que debería repensar la herencia por completo, ya que incluso puede requerir un par de líneas de código adicionales , una solución basada en la composición generalmente resultará mucho más elegante, flexible y a prueba de futuro.

La herencia es realmente una característica genial, pero me temo que se ha usado en exceso en los últimos años. Las personas trataban la herencia como el único martillo que puede clavarlo todo, independientemente de si realmente era un clavo, un tornillo o tal vez algo completamente diferente.

Mecki
fuente
"Muchas personas piensan que la herencia representa nuestro mundo real bastante bien, pero esa no es la verdad". Tanto esto! Al contrario de casi todos los tutoriales de programación en el mundo, modelar objetos del mundo real como cadenas de herencia es probablemente una mala idea a largo plazo. Debería usar la herencia solo cuando haya una relación increíblemente obvia, innata y simple. Me gusta TextFilees a File.
neonblitzer
25

Mi regla general: antes de usar la herencia, considere si la composición tiene más sentido.

Motivo: La subclasificación generalmente significa más complejidad y conectividad, es decir, más difícil de cambiar, mantener y escalar sin cometer errores.

Una respuesta mucho más completa y concreta de Tim Boudreau de Sun:

Los problemas comunes al uso de la herencia como lo veo son:

  • Los actos inocentes pueden tener resultados inesperados : el ejemplo clásico de esto son las llamadas a métodos reemplazables desde el constructor de la superclase, antes de que se inicialicen los campos de instancia de subclases. En un mundo perfecto, nadie haría eso. Este no es un mundo perfecto.
  • Ofrece tentaciones perversas para que los subclases hagan suposiciones sobre el orden de las llamadas a métodos y tal , tales suposiciones tienden a no ser estables si la superclase puede evolucionar con el tiempo. Vea también mi analogía de tostadora y cafetera .
  • Las clases se vuelven más pesadas : no necesariamente sabe qué trabajo está haciendo su superclase en su constructor, o cuánta memoria va a usar. Por lo tanto, construir un objeto inocente y ligero puede ser mucho más costoso de lo que piensas, y esto puede cambiar con el tiempo si la superclase evoluciona
  • Fomenta una explosión de subclases . La carga de clases cuesta tiempo, más clases cuestan memoria. Esto puede no ser un problema hasta que esté lidiando con una aplicación en la escala de NetBeans, pero allí tuvimos problemas reales con, por ejemplo, la lentitud de los menús porque la primera visualización de un menú provocó una carga masiva de clases. Lo arreglamos moviéndonos a una sintaxis más declarativa y otras técnicas, pero eso también costó tiempo en solucionarlo.
  • Hace que sea más difícil cambiar las cosas más adelante : si ha hecho pública una clase, intercambiar la superclase va a romper las subclases, es una elección con la que, una vez que hace público el código, está casado. Entonces, si no está alterando la funcionalidad real de su superclase, obtendrá mucha más libertad para cambiar las cosas más adelante si las usa, en lugar de extender lo que necesita. Tomemos, por ejemplo, la subclasificación de JPanel, esto generalmente es incorrecto; y si la subclase es pública en algún lugar, nunca tendrá la oportunidad de revisar esa decisión. Si se accede como JComponent getThePanel (), aún puede hacerlo (pista: exponga los modelos para los componentes como su API).
  • Las jerarquías de objetos no escalan (o hacer que escalen más tarde es mucho más difícil que planificar con anticipación) : este es el clásico problema de "demasiadas capas". Voy a entrar en esto a continuación, y cómo el patrón AskTheOracle puede resolverlo (aunque puede ofender a los puristas de OOP).

...

Mi opinión sobre qué hacer, si permite la herencia, que puede tomar con un grano de sal es:

  • No exponga campos, nunca, excepto constantes
  • Los métodos serán abstractos o finales.
  • No llame a métodos del constructor de superclase

...

Todo esto se aplica menos a los proyectos pequeños que a los grandes, y menos a las clases privadas que a las públicas.

Peter Tseng
fuente
25

¿Por qué preferir la composición sobre la herencia?

Ver otras respuestas.

¿Cuándo puedes usar la herencia?

A menudo se dice que una clase Barpuede heredar una clase Foocuando la siguiente oración es verdadera:

  1. un bar es un foo

Desafortunadamente, la prueba anterior por sí sola no es confiable. Use lo siguiente en su lugar:

  1. un bar es un foo, Y
  2. los bares pueden hacer todo lo que los foos pueden hacer.

Las primeras pruebas asegura que todos los captadores de Fooalgún sentido en Bar(= propiedades compartidas), mientras que la segunda prueba se asegura de que todos los emisores de Fooalgún sentido en Bar(funcionalidad = compartida).

Ejemplo 1: Perro -> Animal

Un perro es un animal Y los perros pueden hacer todo lo que los animales pueden hacer (como respirar, morir, etc.). Por lo tanto, la clase Dog puede heredar la clase Animal.

Ejemplo 2: Círculo - / -> Elipse

Un círculo es una elipse PERO los círculos no pueden hacer todo lo que pueden hacer las elipses. Por ejemplo, los círculos no pueden estirarse, mientras que las elipses sí. Por lo tanto, la clase Circle no puede heredar la clase Ellipse.

Esto se llama el problema Circle-Ellipse , que realmente no es un problema, solo una prueba clara de que la primera prueba por sí sola no es suficiente para concluir que la herencia es posible. En particular, este ejemplo resalta que las clases derivadas deberían extender la funcionalidad de las clases base, nunca restringirla . De lo contrario, la clase base no podría usarse polimórficamente.

¿Cuándo deberías usar la herencia?

Incluso si se puede utilizar la herencia no significa que usted debe : el uso de la composición es siempre una opción. La herencia es una herramienta poderosa que permite la reutilización implícita de código y el despacho dinámico, pero tiene algunas desventajas, por lo que a menudo se prefiere la composición. Las compensaciones entre herencia y composición no son obvias y, en mi opinión, se explican mejor en la respuesta de lcn .

Como regla general, tiendo a elegir la herencia sobre la composición cuando se espera que el uso polimórfico sea muy común, en cuyo caso el poder del despacho dinámico puede conducir a una API mucho más legible y elegante. Por ejemplo, tener una clase polimórfica Widgeten marcos de GUI, o una clase polimórfica Nodeen bibliotecas XML permite tener una API que es mucho más legible e intuitiva de usar que lo que tendría con una solución basada exclusivamente en la composición.

Principio de sustitución de Liskov

Para que lo sepas, otro método utilizado para determinar si la herencia es posible se llama Principio de sustitución de Liskov :

Las funciones que usan punteros o referencias a clases base deben poder usar objetos de clases derivadas sin saberlo

Esencialmente, esto significa que la herencia es posible si la clase base se puede usar polimórficamente, lo que creo que es equivalente a nuestra prueba "un bar es un foo y los bares pueden hacer todo lo que los foos pueden hacer".

Boris Dalstein
fuente
Los escenarios círculo-elipse y rectángulo cuadrado son ejemplos deficientes. Las subclases son invariablemente más complejas que su superclase, por lo que el problema es artificial. Este problema se resuelve invirtiendo la relación. Una elipse deriva de un círculo y un rectángulo deriva de un cuadrado. Es extremadamente tonto usar composición en estos escenarios.
Fuzzy Logic
@FuzzyLogic De acuerdo, pero de hecho, mi publicación nunca aboga por usar la composición en este caso. Solo he dicho que el problema círculo-elipse es un gran ejemplo de por qué "is-a" no es una buena prueba por sí sola para concluir que Circle debería derivar de Ellipse. Una vez que concluimos que, en realidad, Circle no debería derivar de Ellipse debido a la violación de LSP, las posibles opciones son invertir la relación, o usar composición, o usar clases de plantilla, o usar un diseño más complejo que involucre clases adicionales o funciones auxiliares, etc ... y la decisión, obviamente, debe tomarse caso por caso.
Boris Dalstein
1
@FuzzyLogic Y si tiene curiosidad sobre lo que recomendaría para el caso específico de Circle-Ellipse: recomendaría no implementar la clase Circle. El problema con la inversión de la relación es que también viola el LSP: imagine la función computeArea(Circle* c) { return pi * square(c->radius()); }. Obviamente se rompe si se pasa una Elipse (¿qué significa incluso radio ()?). Una elipse no es un círculo, y como tal no debería derivarse de un círculo.
Boris Dalstein
computeArea(Circle *c) { return pi * width * height / 4.0; }Ahora es genérico.
Fuzzy Logic
2
@FuzzyLogic No estoy de acuerdo: ¿te das cuenta de que esto significa que la clase Circle anticipó la existencia de la clase derivada Ellipse y, por lo tanto, proporcionó width()y height()? ¿Qué pasa si ahora un usuario de la biblioteca decide crear otra clase llamada "EggShape"? ¿Debería derivarse también de "Círculo"? Por supuesto no. Una forma de huevo no es un círculo, y una elipse tampoco es un círculo, por lo que ninguno debe derivarse de Circle ya que rompe LSP. Los métodos que realizan la operación en una clase Circle * hacen fuertes suposiciones sobre lo que es un círculo, y romper estas suposiciones casi con seguridad conducirá a errores.
Boris Dalstein
19

La herencia es muy poderosa, pero no puedes forzarla (ver: el problema círculo-elipse ). Si realmente no puede estar completamente seguro de una verdadera relación de subtipo "es-a", entonces es mejor ir con la composición.

yukondude
fuente
15

La herencia crea una fuerte relación entre una subclase y una superclase; La subclase debe conocer los detalles de implementación de las superclases. Crear la superclase es mucho más difícil cuando tienes que pensar cómo se puede extender. Debe documentar cuidadosamente los invariantes de clase y establecer qué otros métodos reemplazables utilizan internamente.

La herencia a veces es útil, si la jerarquía realmente representa una relación es-es-una. Se relaciona con el Principio Abierto-Cerrado, que establece que las clases deben cerrarse para su modificación pero abrirse para la extensión. De esa manera puedes tener polimorfismo; tener un método genérico que se ocupe del supertipo y sus métodos, pero mediante el envío dinámico se invoca el método de la subclase. Esto es flexible y ayuda a crear indirección, que es esencial en el software (para saber menos sobre los detalles de implementación).

Sin embargo, la herencia se usa en exceso fácilmente y crea una complejidad adicional, con dependencias difíciles entre las clases. También entender lo que sucede durante la ejecución de un programa se vuelve bastante difícil debido a las capas y la selección dinámica de las llamadas a métodos.

Sugeriría usar la composición como predeterminada. Es más modular y ofrece el beneficio de un enlace tardío (puede cambiar el componente dinámicamente). También es más fácil probar las cosas por separado. Y si necesita utilizar un método de una clase, no está obligado a tener cierta forma (Principio de sustitución de Liskov).

egaga
fuente
3
Vale la pena señalar que la herencia no es la única forma de lograr el polimorfismo. El patrón decorador proporciona la apariencia de polimorfismo a través de la composición.
BitMask777
1
@ BitMask777: El polimorfismo de subtipo es solo un tipo de polimorfismo, otro sería el polimorfismo paramétrico, no necesita herencia para eso. También más importante: cuando se habla de herencia, uno significa herencia de clase; .ie puede tener un polimorfismo de subtipo al tener una interfaz común para múltiples clases, y no se obtienen los problemas de herencia.
Egaga
2
@engaga: Interpreté tu comentario Inheritance is sometimes useful... That way you can have polymorphismcomo un vínculo rígido con los conceptos de herencia y polimorfismo (se supone que el contexto es subtipo). Mi comentario tenía la intención de señalar lo que aclaras en tu comentario: que la herencia no es la única forma de implementar el polimorfismo, y de hecho no es necesariamente el factor determinante al decidir entre la composición y la herencia.
BitMask777
15

Supongamos que un avión tiene solo dos partes: un motor y alas.
Luego hay dos formas de diseñar una clase de avión.

Class Aircraft extends Engine{
  var wings;
}

Ahora su avión puede comenzar con alas fijas
y cambiarlas a alas giratorias sobre la marcha. Es esencialmente
un motor con alas. Pero, ¿y si también quisiera cambiar
el motor sobre la marcha?

O la clase base Engineexpone a un mutador para cambiar sus
propiedades, o rediseño Aircraftcomo:

Class Aircraft {
  var wings;
  var engine;
}

Ahora, también puedo reemplazar mi motor sobre la marcha.

simplfuzz
fuente
Su publicación muestra un punto que no había considerado antes: para continuar con su analogía de objetos mecánicos con múltiples partes, en algo así como un arma de fuego, generalmente hay una parte marcada con un número de serie, cuyo número de serie se considera la del arma de fuego en su conjunto (para una pistola, normalmente sería el armazón). Uno puede reemplazar todas las otras partes y aún tener la misma arma de fuego, pero si el marco se agrieta y necesita ser reemplazado, el resultado de ensamblar un nuevo marco con todas las otras partes de la pistola original sería una pistola nueva. Tenga en cuenta que ...
supercat
... el hecho de que varias partes de una pistola puedan tener números de serie marcados en ellas no significa que una pistola pueda tener múltiples identidades. Solo el número de serie en el marco identifica el arma; el número de serie en cualquier otra parte identifica con qué arma se fabricaron esas piezas para ensamblar, que puede no ser la pistola con la que se ensamblan en un momento dado.
supercat
7

Cuando desea "copiar" / Exponer la API de la clase base, usa la herencia. Cuando solo desee "copiar" la funcionalidad, use la delegación.

Un ejemplo de esto: desea crear una pila de una lista. La pila solo tiene pop, push y peek. No debe usar la herencia dado que no desea la funcionalidad de tipo push_back, push_front, removeAt, et al.en una pila.

Anzurio
fuente
7

Estas dos formas pueden convivir bien y apoyarse mutuamente.

La composición es solo modular: crea una interfaz similar a la clase principal, crea un nuevo objeto y le delega llamadas. Si estos objetos no necesitan conocerse entre sí, es una composición bastante segura y fácil de usar. Hay tantas posibilidades aquí.

Sin embargo, si la clase principal por alguna razón necesita acceder a las funciones proporcionadas por la "clase secundaria" para programadores sin experiencia, puede parecer que es un gran lugar para usar la herencia. La clase padre simplemente puede llamar a su propio resumen "foo ()", que es sobrescrito por la subclase y luego puede dar el valor a la base abstracta.

Parece una buena idea, pero en muchos casos es mejor simplemente darle a la clase un objeto que implemente foo () (o incluso establecer el valor proporcionado por foo () manualmente) que heredar la nueva clase de alguna clase base que requiere la función foo () que se especificará.

¿Por qué?

Porque la herencia es una forma pobre de mover información .

La composición tiene una ventaja real aquí: la relación se puede revertir: la "clase principal" o el "trabajador abstracto" pueden agregar cualquier objeto "secundario" específico que implemente cierta interfaz + cualquier elemento secundario se puede establecer dentro de cualquier otro tipo de elemento primario, que acepta Es tipo . Y puede haber cualquier cantidad de objetos, por ejemplo, MergeSort o QuickSort podrían ordenar cualquier lista de objetos que implementen una interfaz de comparación abstracta. O para decirlo de otra manera: cualquier grupo de objetos que implementen "foo ()" y otro grupo de objetos que puedan usar objetos que tengan "foo ()" pueden jugar juntos.

Puedo pensar en tres razones reales para usar la herencia:

  1. Tienes muchas clases con la misma interfaz y quieres ahorrar tiempo escribiéndolas
  2. Tienes que usar la misma clase base para cada objeto
  3. Debe modificar las variables privadas, que no pueden ser públicas en ningún caso.

Si esto es cierto, entonces probablemente sea necesario usar la herencia.

No hay nada malo en usar la razón 1, es muy bueno tener una interfaz sólida en sus objetos. Esto se puede hacer usando composición o con herencia, no hay problema, si esta interfaz es simple y no cambia. Por lo general, la herencia es bastante efectiva aquí.

Si el motivo es el número 2, se vuelve un poco complicado. ¿Realmente solo necesitas usar la misma clase base? En general, solo usar la misma clase base no es lo suficientemente bueno, pero puede ser un requisito de su marco, una consideración de diseño que no se puede evitar.

Sin embargo, si desea utilizar las variables privadas, el caso 3, entonces puede estar en problemas. Si considera que las variables globales no son seguras, debería considerar el uso de la herencia para obtener acceso a variables privadas que también son inseguras . Eso sí, las variables globales no son tan malas: las bases de datos son esencialmente un gran conjunto de variables globales. Pero si puedes manejarlo, entonces está bastante bien.

Tero Tolonen
fuente
7

Para abordar esta pregunta desde una perspectiva diferente para los programadores más nuevos:

La herencia a menudo se enseña temprano cuando aprendemos programación orientada a objetos, por lo que se ve como una solución fácil a un problema común.

Tengo tres clases que necesitan una funcionalidad común. Entonces, si escribo una clase base y hago que todos hereden de ella, entonces todos tendrán esa funcionalidad y solo tendré que mantenerla en un lugar.

Suena genial, pero en la práctica casi nunca funciona, por una de varias razones:

  • Descubrimos que hay otras funciones que queremos que tengan nuestras clases. Si la forma en que agregamos funcionalidad a las clases es a través de la herencia, tenemos que decidir: ¿lo agregamos a la clase base existente, aunque no todas las clases que heredan de ella necesitan esa funcionalidad? ¿Creamos otra clase base? Pero, ¿qué pasa con las clases que ya heredan de la otra clase base?
  • Descubrimos que solo para una de las clases que hereda de nuestra clase base, queremos que la clase base se comporte de manera un poco diferente. Así que ahora volvemos y jugamos con nuestra clase base, quizás agregando algunos métodos virtuales, o peor aún, algún código que diga: "Si heredé el tipo A, haga esto, pero si heredé el tipo B, haga eso ". Eso es malo por muchas razones. Una es que cada vez que cambiamos la clase base, estamos cambiando efectivamente cada clase heredada. Así que realmente estamos cambiando las clases A, B, C y D porque necesitamos un comportamiento ligeramente diferente en la clase A. Por más cuidadosos que creamos que somos, podríamos romper una de esas clases por razones que no tienen nada que ver con esas clases
  • Es posible que sepamos por qué decidimos hacer que todas estas clases se hereden entre sí, pero es posible que no tenga sentido (probablemente no) para alguien que tenga que mantener nuestro código. Podríamos obligarlos a tomar una decisión difícil: ¿hago algo realmente feo y desordenado para hacer el cambio que necesito (ver el punto anterior) o simplemente reescribo un montón de esto?

Al final, vinculamos nuestro código en algunos nudos difíciles y no obtenemos ningún beneficio, excepto que podemos decir: "Genial, aprendí sobre la herencia y ahora lo usé". Eso no pretende ser condescendiente porque todos lo hemos hecho. Pero todos lo hicimos porque nadie nos dijo que no lo hiciéramos.

Tan pronto como alguien me explicó "favorecer la composición sobre la herencia", pensé en cada vez que intentaba compartir la funcionalidad entre las clases que usaban la herencia y me di cuenta de que la mayoría de las veces no funcionaba bien.

El antídoto es el principio de responsabilidad única . Piense en ello como una restricción. Mi clase debe hacer una cosa. Yo debo ser capaz de dar mi clase un nombre que de alguna manera describe que una cosa que hace. (Hay excepciones para todo, pero las reglas absolutas a veces son mejores cuando estamos aprendiendo). De esto se deduce que no puedo escribir una clase base llamada ObjectBaseThatContainsVariousFunctionsNeededByDifferentClasses. Cualquier funcionalidad distinta que necesite debe estar en su propia clase, y luego otras clases que necesiten esa funcionalidad pueden depender de esa clase, no heredar de ella.

A riesgo de simplificar demasiado, eso es composición: componer varias clases para trabajar juntas. Y una vez que formamos ese hábito, encontramos que es mucho más flexible, mantenible y comprobable que usar la herencia.

Scott Hannen
fuente
Que las clases no puedan usar múltiples clases base no es una mala reflexión sobre la Herencia, sino una pobre reflexión sobre la falta de capacidad de un idioma en particular.
iPherian
En el tiempo transcurrido desde que escribí esta respuesta, leí esta publicación del "Tío Bob" que aborda esa falta de capacidad. Nunca he usado un lenguaje que permita la herencia múltiple. Pero mirando hacia atrás, la pregunta está etiquetada como "independiente del lenguaje" y mi respuesta asume C #. Necesito ampliar mis horizontes.
Scott Hannen el
6

Aparte de es un / tiene algunas consideraciones, uno también debe considerar la "profundidad" de la herencia por la que tiene que pasar su objeto. Cualquier cosa que supere los cinco o seis niveles de herencia profunda puede causar problemas inesperados de conversión y boxing / unboxing, y en esos casos puede ser conveniente componer su objeto.

Jon Limjap
fuente
6

Cuando tienes una relación is-a entre dos clases (por ejemplo, el perro es un canino), optas por la herencia.

Por otro lado, cuando tiene una relación ad - adjetiva o adjetiva entre dos clases (el estudiante tiene cursos) o (cursos de estudios de docentes), elige la composición.

Amir Aslam
fuente
Dijiste herencia y herencia. ¿no te refieres a herencia y composición?
trevorKirkby
No, tu no. También puede definir una interfaz canina y dejar que cada perro la implemente y terminará con más código SÓLIDO.
markus
5

Una manera simple de dar sentido a esto sería que la herencia se debe usar cuando necesita que un objeto de su clase tenga la misma interfaz que su clase principal, de modo que pueda tratarse como un objeto de la clase principal (transmisión ascendente) . Además, las llamadas a funciones en un objeto de clase derivado seguirían siendo las mismas en todas partes en el código, pero el método específico para llamar se determinaría en tiempo de ejecución (es decir, la implementación de bajo nivel difiere, la interfaz de alto nivel sigue siendo la misma).

La composición debe usarse cuando no necesita que la nueva clase tenga la misma interfaz, es decir, desea ocultar ciertos aspectos de la implementación de la clase que el usuario de esa clase no necesita conocer. Por lo tanto, la composición es más compatible con la encapsulación (es decir, ocultando la implementación), mientras que la herencia está destinada a admitir la abstracción (es decir, proporcionar una representación simplificada de algo, en este caso la misma interfaz para una gama de tipos con diferentes componentes internos).

YS
fuente
+1 por mención de interfaz. Utilizo este enfoque a menudo para ocultar las clases existentes y hacer que mi nueva clase se pueda probar adecuadamente en la unidad burlándose del objeto utilizado para la composición. Esto requiere que el propietario del nuevo objeto pase la clase padre candidata en su lugar.
Kell
4

El subtipo es apropiado y más poderoso cuando se pueden enumerar los invariantes , de lo contrario, use la composición de funciones para la extensibilidad.

Shelby Moore III
fuente
4

Estoy de acuerdo con @Pavel, cuando dice que hay lugares para la composición y hay lugares para la herencia.

Creo que la herencia debería usarse si su respuesta es afirmativa a cualquiera de estas preguntas.

  • ¿Es su clase parte de una estructura que se beneficia del polimorfismo? Por ejemplo, si tuviera una clase Shape, que declara un método llamado draw (), entonces claramente necesitamos que las clases Circle y Square sean subclases de Shape, de modo que sus clases cliente dependan de Shape y no de subclases específicas.
  • ¿Necesita su clase reutilizar las interacciones de alto nivel definidas en otra clase? El patrón de diseño del método de plantilla sería imposible de implementar sin herencia. Creo que todos los marcos extensibles usan este patrón.

Sin embargo, si su intención es puramente la reutilización del código, entonces la composición probablemente sea una mejor opción de diseño.

Parag
fuente
4

La herencia es un mecanismo muy poderoso para la reutilización de código. Pero necesita ser utilizado correctamente. Yo diría que la herencia se usa correctamente si la subclase también es un subtipo de la clase padre. Como se mencionó anteriormente, el Principio de sustitución de Liskov es el punto clave aquí.

Subclase no es lo mismo que subtipo. Puede crear subclases que no sean subtipos (y aquí es cuando debe usar composición). Para entender qué es un subtipo, comencemos dando una explicación de qué es un tipo.

Cuando decimos que el número 5 es de tipo entero, estamos afirmando que 5 pertenece a un conjunto de valores posibles (como ejemplo, vea los valores posibles para los tipos primitivos de Java). También estamos afirmando que hay un conjunto válido de métodos que puedo realizar en el valor, como la suma y la resta. Y finalmente estamos afirmando que hay un conjunto de propiedades que siempre se satisfacen, por ejemplo, si agrego los valores 3 y 5, obtendré 8 como resultado.

Para dar otro ejemplo, piense en los tipos de datos abstractos, Conjunto de enteros y Lista de enteros, los valores que pueden contener están restringidos a enteros. Ambos admiten un conjunto de métodos, como add (newValue) y size (). Y ambos tienen propiedades diferentes (invariante de clase), Sets no permite duplicados, mientras que List sí permite duplicados (por supuesto, hay otras propiedades que ambos satisfacen).

El subtipo también es un tipo, que tiene una relación con otro tipo, llamado tipo padre (o supertipo). El subtipo debe satisfacer las características (valores, métodos y propiedades) del tipo primario. La relación significa que en cualquier contexto donde se espera el supertipo, puede ser sustituible por un subtipo, sin afectar el comportamiento de la ejecución. Vamos a ver un código para ejemplificar lo que estoy diciendo. Supongamos que escribo una lista de enteros (en algún tipo de pseudo lenguaje):

class List {
  data = new Array();

  Integer size() {
    return data.length;
  }

  add(Integer anInteger) {
    data[data.length] = anInteger;
  }
}

Luego, escribo el Conjunto de enteros como una subclase de la Lista de enteros:

class Set, inheriting from: List {
  add(Integer anInteger) {
     if (data.notContains(anInteger)) {
       super.add(anInteger);
     }
  }
}

Nuestra clase Conjunto de enteros es una subclase de la Lista de enteros, pero no es un subtipo, ya que no satisface todas las características de la clase Lista. Los valores y la firma de los métodos se satisfacen pero las propiedades no. El comportamiento del método add (Integer) ha cambiado claramente, sin preservar las propiedades del tipo primario. Piensa desde el punto de vista del cliente de tus clases. Pueden recibir un conjunto de enteros donde se espera una lista de enteros. El cliente puede querer agregar un valor y obtener ese valor agregado a la Lista incluso si ese valor ya existe en la Lista. Pero ella no obtendrá ese comportamiento si el valor existe. ¡Una gran sorpresa para ella!

Este es un ejemplo clásico de un uso incorrecto de la herencia. Usar composición en este caso.

(un fragmento de: use la herencia correctamente ).

Enrique Molinari
fuente
3

Una regla general que he escuchado es que la herencia debe usarse cuando se trata de una relación "es-a" y la composición cuando es un "has-a". Incluso con eso, siento que siempre debes inclinarte hacia la composición porque elimina mucha complejidad.


fuente
2

Composición v / s La herencia es un tema amplio. No hay una respuesta real para lo que es mejor, ya que creo que todo depende del diseño del sistema.

En general, el tipo de relación entre objetos proporciona mejor información para elegir uno de ellos.

Si el tipo de relación es "IS-A", la herencia es un mejor enfoque. de lo contrario, el tipo de relación es relación "HAS-A", entonces la composición se acercará mejor.

Es totalmente dependiente de la relación de la entidad.

Shami Qureshi
fuente
2

Aunque se prefiere la Composición, me gustaría destacar los pros de la Herencia y los contras de la Composición .

Ventajas de la herencia:

  1. Establece una relación lógica " ES A" . Si el automóvil y el camión son dos tipos de vehículos (clase base), la clase secundaria ES A clase base.

    es decir

    El auto es un vehículo

    El camión es un vehículo

  2. Con la herencia, puede definir / modificar / ampliar una capacidad

    1. La clase base no proporciona implementación y la subclase tiene que anular el método completo (resumen) => Puede implementar un contrato
    2. La clase base proporciona una implementación predeterminada y la subclase puede cambiar el comportamiento => Puede redefinir el contrato
    3. La subclase agrega extensión a la implementación de la clase base llamando a super.methodName () como primera instrucción => Puede extender un contrato
    4. La clase base define la estructura del algoritmo y la subclase anulará una parte del algoritmo => Puede implementar Template_method sin cambios en el esqueleto de la clase base

Contras de composición:

  1. En herencia, la subclase puede invocar directamente el método de la clase base aunque no esté implementando el método de la clase base debido a la relación IS A. Si usa composición, debe agregar métodos en la clase contenedor para exponer la clase contenida API

Por ejemplo, si el automóvil contiene el vehículo y si tiene que obtener el precio del automóvil , que se ha definido en el vehículo , su código será así

class Vehicle{
     protected double getPrice(){
          // return price
     }
} 

class Car{
     Vehicle vehicle;
     protected double getPrice(){
          return vehicle.getPrice();
     }
} 
Ravindra babu
fuente
Creo que no responde la pregunta
almanegra
Puede volver a mirar la pregunta OP. He abordado: ¿Qué compensaciones hay para cada enfoque?
Ravindra babu
Como mencionó, solo habla sobre "ventajas y desventajas de la composición", y no sobre las compensaciones de CADA enfoque o los casos en los que debe usar uno sobre otro
almanegra
Los pros y los contras ofrecen una compensación, ya que los pros de la herencia son los contras de la composición y los contras de la composición son los pros de la herencia.
Ravindra babu
1

Como muchas personas dijeron, primero comenzaré con la verificación, si existe una relación "es-a". Si existe, generalmente verifico lo siguiente:

Si la clase base puede ser instanciada. Es decir, si la clase base puede ser no abstracta. Si puede ser no abstracto, generalmente prefiero la composición

Ej. 1. El contador es un empleado. Pero no usaré la herencia porque se puede crear una instancia de un objeto Employee.

Ej. 2. El libro es un artículo de venta. Un artículo de venta no puede ser instanciado, es un concepto abstracto. Por lo tanto, usaré la herencia. SellingItem es una clase base abstracta (o interfaz en C #)

¿Qué opinas sobre este enfoque?

Además, apoyo @anon answer en ¿Por qué usar herencia en absoluto?

La razón principal para usar la herencia no es como una forma de composición, es para que pueda obtener un comportamiento polimórfico. Si no necesita polimorfismo, probablemente no debería estar usando la herencia.

@MatthieuM. dice en /software/12439/code-smell-inheritance-abuse/12448#comment303759_12448

El problema con la herencia es que puede usarse para dos propósitos ortogonales:

interfaz (para polimorfismo)

implementación (para reutilización de código)

REFERENCIA

  1. ¿Qué diseño de clase es mejor?
  2. Herencia vs. Agregación
LCJ
fuente
1
No estoy seguro de por qué "la clase base es abstracta". figura en la discusión ... el LSP: ¿funcionarán todas las funciones que operan en Dogs si se pasan objetos Poodle? En caso afirmativo, entonces Poodle se puede sustituir por Dog y, por lo tanto, se puede heredar de Dog.
Gishu
@Gishu Gracias. Definitivamente voy a investigar LSP. Pero antes de eso, ¿puede proporcionar un "ejemplo donde la herencia es adecuada en la que la clase base no puede ser abstracta". Lo que creo es que la herencia es aplicable solo si la clase base es abstracta. Si la clase base necesita ser instanciada por separado, no elija la herencia. Es decir, aunque el contador sea un empleado, no use la herencia.
LCJ
1
He estado leyendo WCF últimamente. Un ejemplo en el marco .net es SynchronizationContext (base + puede ser instanciado) que las colas funcionan en un hilo ThreadPool. Derivaciones incluyen WinFormsSyncContext (cola en la interfaz de usuario de rosca) y DispatcherSyncContext (cola en WPF Dispatcher)
Gishu
@Gishu Gracias. Sin embargo, sería más útil si puede proporcionar un escenario basado en el dominio del Banco, el dominio de recursos humanos, el dominio minorista o cualquier otro dominio popular.
LCJ
1
Lo siento. No estoy familiarizado con esos dominios. Otro ejemplo si el anterior era demasiado obtuso es la clase Control en Winforms / WPF. El control base / genérico puede ser instanciado. Las derivaciones incluyen cuadros de lista, cuadros de texto, etc. Ahora que lo pienso, el patrón de diseño de decorador es un buen ejemplo en mi humilde opinión y útil también. El decorador deriva del objeto no abstracto que quiere envolver / decorar.
Gishu
1

Veo que nadie mencionó el problema del diamante , que podría surgir con la herencia.

De un vistazo, si las clases B y C heredan A y ambos anulan el método X, y una cuarta clase D, hereda de B y C y no anula X, ¿qué implementación de XD se supone que debe usar?

Wikipedia ofrece una buena visión general del tema que se discute en esta pregunta.

Veverke
fuente
1
D hereda B y C no A. Si es así, usaría la implementación de X que está en la clase A.
fabricio
1
@fabricio: gracias, edité el texto. Por cierto, tal escenario no puede ocurrir en idiomas que no permiten la herencia de clases múltiples, ¿estoy en lo cierto?
Veverke
sí, tienes razón ... y nunca he trabajado con uno que permita la herencia múltiple (según el ejemplo en el problema del diamante) ...
fabricio