¿De dónde proviene este concepto de "favorecer la composición sobre la herencia"?

137

En los últimos meses, el mantra "favorecer la composición sobre la herencia" parece haber surgido de la nada y convertirse en casi algún tipo de meme dentro de la comunidad de programación. Y cada vez que lo veo, estoy un poco desconcertado. Es como si alguien dijera "favorece a los taladros sobre los martillos". En mi experiencia, la composición y la herencia son dos herramientas diferentes con diferentes casos de uso, y tratarlas como si fueran intercambiables y una de ellas era inherentemente superior a la otra no tiene sentido.

Además, nunca veo una explicación real para por qué la herencia es mala y la composición es buena, lo que me hace más sospechoso. ¿Se supone que solo debe ser aceptado en la fe? La sustitución de Liskov y el polimorfismo tienen beneficios bien conocidos y bien definidos, y la OMI comprende todo el sentido de usar la programación orientada a objetos, y nadie explica por qué deben descartarse en favor de la composición.

¿Alguien sabe de dónde viene este concepto y cuál es la razón detrás de él?

    
pregunta Mason Wheeler 05.04.2011 - 00:54

14 respuestas

131

Aunque creo que he escuchado discusiones de composición frente a herencia mucho antes de GoF, no puedo señalar una fuente específica. Podría haber sido Booch de todos modos.

< rant >

Ah, pero como muchos mantras, este ha degenerado en las líneas típicas:

  1. se presenta con una explicación y un argumento detallados por una fuente muy respetada que acuña la frase como un recordatorio de la compleja discusión original
  2. se comparte con un poco de conocimiento de parte del club por unos pocos informados por un tiempo, generalmente cuando se comenta errores n00b
  3. pronto lo repiten sin pensar miles de personas que nunca leen la explicación, pero les encanta usarla como excusa para no pensar , y como una forma barata y fácil de sentirse superior a los demás
  4. eventualmente, ninguna cantidad de desacreditación razonable puede contener la marea "meme", y el paradigma degenera en religión y dogma.

El meme, originalmente destinado a llevar n00bs a la iluminación, ahora se usa como un palo para golpearlos inconsciente.

La composición y la herencia son cosas muy diferentes, y no deben confundirse entre sí. Si bien es cierto que la composición se puede usar para simular la herencia con mucho trabajo adicional , esto no hace que la herencia sea un ciudadano de segunda clase, ni hace que la composición sea el hijo favorito. El hecho de que muchos n00bs intenten usar la herencia como acceso directo no invalida el mecanismo, y casi todos los n00bs aprenden del error y, por lo tanto, mejoran.

PIENSE acerca de tus diseños y deja de emitir eslóganes.

< / rant >

    
respondido por el Steven A. Lowe 19.04.2012 - 03:55
72

Experiencia.

Como usted dice que son herramientas para diferentes trabajos, pero la frase surgió porque la gente no lo usaba de esa manera.

La herencia es principalmente una herramienta polimórfica, pero algunas personas, para su posterior peligro, intentan usarla como una forma de reutilizar / compartir el código. La razón es que "bien, si heredo, obtengo todos los métodos de forma gratuita", pero ignorando el hecho de que estas dos clases potencialmente no tienen una relación polimórfica.

Entonces, ¿por qué favorecer la composición sobre la herencia? bueno, simplemente porque la mayoría de las veces la relación entre clases no es polimórfica. Existe simplemente para ayudar a recordar a las personas que no reaccionen de forma espontánea heredando.

    
respondido por el Stephen Bailey 05.04.2011 - 06:40
40

No es una idea nueva, creo que en realidad se introdujo en el libro de patrones de diseño de GoF, que se publicó en 1994.

El principal problema con la herencia es que se trata de una caja blanca. Por definición, necesita conocer los detalles de implementación de la clase de la que está heredando. Por otro lado, con la composición, solo te importa la interfaz pública de la clase que estás componiendo.

Del libro de GoF:

  

La herencia expone una subclase a los detalles de la implementación de la matriz, a menudo se dice que 'la herencia rompe la encapsulación'

El artículo de wikipedia en el libro de GoF tiene una introducción decente.

    
respondido por el Dean Harding 05.04.2011 - 01:08
26

Para responder a parte de su pregunta, creo que este concepto apareció por primera vez en el GOF Patrones de diseño: elementos de objetos reutilizables orientados Libro de software , publicado por primera vez en 1994. La frase aparece en la parte superior de la página 20, en la introducción:

  

Favorecer la composición del objeto sobre la herencia

Prologan esta declaración con una breve comparación de la herencia con la composición. No dicen "nunca usar herencia".

    
respondido por el vjones 05.04.2011 - 01:07
22

"Composición sobre herencia" es una forma breve (y aparentemente engañosa) de decir "Cuando se siente que los datos (o el comportamiento) de una clase deben incorporarse a otra clase, siempre considere usar la composición antes de aplicar ciegamente la herencia".

¿Por qué es esto cierto? Porque la herencia crea un acoplamiento apretado en tiempo de compilación entre las 2 clases. En contraste, la composición es un acoplamiento flojo, que entre otros permite una clara separación de preocupaciones, la posibilidad de cambiar las dependencias en tiempo de ejecución y una verificación de dependencias más sencilla y aislada.

Eso solo significa que la herencia debe manejarse con cuidado porque tiene un costo, no es que no sea útil. En realidad, "Composición sobre herencia" a menudo termina siendo "Composición + herencia sobre herencia", ya que a menudo desea que su dependencia compuesta sea una superclase abstracta en lugar de la propia subclase concreta. Le permite cambiar entre diferentes implementaciones concretas de su dependencia en tiempo de ejecución.

Por ese motivo (entre otros), es probable que vea la herencia utilizada más a menudo en forma de implementación de interfaz o clases abstractas que la herencia vainilla.

Un ejemplo (metafórico) podría ser:

"Tengo una clase de Snake y quiero incluir como parte de esa clase lo que sucede cuando Snake muerde. Estaría tentado de que Snake heredara una clase BiterAnimal que tiene el método Bite () y anulará ese método para Refleja la mordedura venenosa. Pero Composición sobre herencia me advierte que debo intentar usar la composición ... En mi caso, esto podría traducirse en que la serpiente tenga un miembro de mordedura. La clase de mordedura puede ser abstracta (o una interfaz) con varias subclases. Esto me permitiría cosas agradables como tener subclases de VenomousBite y DryBite y poder cambiar de mordida en la misma instancia de Snake a medida que la serpiente crece con la edad. Además, manejar todos los efectos de una mordida en su propia clase separada podría permitirle reutilizarla. esa clase Frost, porque la escarcha pica pero no es un BiterAnimal, y así sucesivamente ... "

    
respondido por el guillaume31 06.04.2011 - 17:15
22

Algunos posibles argumentos para la composición:

La composición es un poco más independiente del lenguaje / marco
La herencia y lo que impone / requiere / habilita diferirá entre los idiomas en términos de lo que la sub / superclase tiene acceso y las implicaciones de rendimiento que puede tener en los métodos virtuales, etc. La composición es bastante básica y requiere muy poco soporte de lenguaje, y por lo tanto implementaciones a través de diferentes plataformas / marcos pueden compartir patrones de composición más fácilmente.

La composición es una forma muy sencilla y táctil de construir objetos
La herencia es relativamente fácil de entender, pero aún no es tan fácil de demostrar en la vida real. Muchos objetos en la vida real se pueden dividir en partes y componer. Digamos que una bicicleta se puede construir con dos ruedas, un cuadro, un asiento, una cadena, etc. Se explica fácilmente por la composición. Mientras que en una metáfora de la herencia se podría decir que una bicicleta extiende un monociclo, de alguna manera es posible, pero aún mucho más lejos de la imagen real que de la composición (obviamente este no es un buen ejemplo de herencia, pero el punto sigue siendo el mismo). Incluso la palabra herencia (al menos de la mayoría de los hablantes de inglés de EE. UU. Que yo esperaría) invoca automáticamente un significado como "Algo pasado de un pariente fallecido" que tiene cierta correlación con su significado en el software, pero que aún no encaja. p>

La composición es casi siempre más flexible
Usando la composición, siempre puede elegir definir su propio comportamiento o simplemente exponer esa parte de sus partes compuestas. De esta manera, no tendrá ninguna de las restricciones que puede imponer una jerarquía de herencia (virtual vs. no virtual, etc.)

Por lo tanto, podría ser porque la Composición es, naturalmente, una metáfora más simple que tiene menos restricciones teóricas que la herencia. Además, estas razones particulares pueden ser más evidentes durante el tiempo de diseño, o posiblemente sobresalir cuando se trata de algunos de los puntos débiles de la herencia.

Descargo de responsabilidad:
Obviamente no se trata de una calle clara. Cada diseño merece la evaluación de varios patrones / herramientas. La herencia se usa ampliamente, tiene muchos beneficios y muchas veces es más elegante que la composición. Estas son solo algunas de las posibles razones que uno podría usar al favorecer la composición.

    
respondido por el TJB 07.02.2013 - 21:00
10

Tal vez haya notado que la gente lo ha dicho en los últimos meses, pero los programadores lo han sabido durante mucho más tiempo. Ciertamente lo he estado diciendo donde sea apropiado durante aproximadamente una década.

El punto del concepto es que hay una gran sobrecarga conceptual en la herencia. Cuando está utilizando la herencia, entonces cada llamada a un solo método tiene un envío implícito. Si tiene árboles de herencia profunda, o envío múltiple, o (aún peor) ambos, entonces averiguar dónde se enviará el método en particular en cualquier llamada particular puede convertirse en un PITA real. Hace que el razonamiento correcto sobre el código sea más complejo y hace que la depuración sea más difícil.

Déjame dar un ejemplo simple para ilustrar. Supongamos que en lo profundo de un árbol de herencia, alguien llamó a un método foo . Luego aparece alguien más y agrega foo en la parte superior del árbol, pero haciendo algo diferente. (Este caso es más común con herencia múltiple). Ahora, esa persona que trabaja en la clase raíz ha roto la oscura clase infantil y probablemente no se da cuenta. Podría tener una cobertura del 100% con las pruebas unitarias y no notar esta ruptura porque la persona en la parte superior no pensaría en probar la clase infantil, y las pruebas para la clase infantil no piensan en probar los nuevos métodos creados en la parte superior . (Es cierto que hay formas de escribir pruebas unitarias que detectarán esto, pero también hay casos en los que no se pueden escribir fácilmente las pruebas de esa manera).

Por el contrario, cuando usa la composición, en cada llamada generalmente queda más claro a qué está enviando la llamada. (De acuerdo, si está utilizando la inversión de control, por ejemplo, con la inyección de dependencia, entonces averiguar a dónde va la llamada también puede ser problemático. Pero generalmente es más fácil de descifrar). Esto hace que el razonamiento sea más fácil. Como beneficio adicional, la composición resulta en tener métodos separados entre sí. El ejemplo anterior no debería ocurrir allí porque la clase secundaria se movería a algún componente oscuro, y nunca hay dudas sobre si la llamada a foo estaba destinada al componente oscuro o al objeto principal.

Ahora tiene toda la razón, ya que la herencia y la composición son dos herramientas muy diferentes que sirven a dos tipos diferentes de cosas. La herencia segura conlleva una sobrecarga conceptual, pero cuando es la herramienta adecuada para el trabajo, conlleva menos sobrecarga conceptual que tratar de no usarla y hacer a mano lo que hace por usted. Nadie que sepa lo que están haciendo diría que nunca debes usar la herencia. Pero asegúrate de que sea lo correcto.

Desafortunadamente, muchos desarrolladores aprenden sobre el software orientado a objetos, aprenden sobre la herencia y luego salen a usar su nuevo hacha con la mayor frecuencia posible. Lo que significa que intentan usar la herencia donde la composición era la herramienta correcta. Es de esperar que aprendan mejor a tiempo, pero con frecuencia esto no ocurre hasta después de algunas extremidades extraídas, etc. Decirles desde el principio que es una mala idea tiende a acelerar el proceso de aprendizaje y reducir las lesiones.

    
respondido por el btilly 05.04.2011 - 01:30
8

Es una reacción a la observación de que los principiantes de OO tienden a usar la herencia cuando no lo necesitan. La herencia no es ciertamente algo malo, pero puede ser usada en exceso. Si una clase solo necesita funcionalidad de otra, entonces la composición probablemente funcionará. En otras circunstancias, la herencia funcionará y la composición no.

Heredar de una clase implica muchas cosas. Implica que un Derivado es un tipo de Base (consulte el Principio de Sustitución de Liskov para obtener información detallada), siempre que utilizar una Base tendría sentido utilizar un Derivado. Proporciona acceso derivado a los miembros protegidos y funciones de miembro de Base. Es una relación cercana, lo que significa que tiene un alto acoplamiento, y es más probable que los cambios en una requieran cambios en la otra.

El acoplamiento es una mala cosa. Hace que los programas sean más difíciles de entender y modificar. En igualdad de condiciones, siempre debe seleccionar la opción con menos acoplamiento.

Por lo tanto, si la composición o la herencia harán el trabajo de manera efectiva, elija la composición, porque es un acoplamiento más bajo. Si la composición no hace el trabajo con eficacia, y la herencia lo hará, elija la herencia, porque tiene que hacerlo.

    
respondido por el David Thornley 05.04.2011 - 16:55
8

Aquí están mis dos centavos (más allá de todos los puntos excelentes ya planteados):

En mi humilde opinión, todo se reduce al hecho de que la mayoría de los programadores realmente no obtienen herencia y terminan exagerando, en parte como resultado de la enseñanza de este concepto. Este concepto existe como una forma de tratar de disuadirlos de una exageración, en lugar de centrarse en enseñarles cómo hacerlo correctamente.

Cualquier persona que haya dedicado tiempo a la enseñanza o la tutoría sabe que esto es lo que sucede a menudo, especialmente con los nuevos desarrolladores que tienen experiencia en otros paradigmas:

Estos desarrolladores inicialmente sienten que la herencia es este concepto aterrador. Así que terminan creando tipos con superposiciones de interfaz (por ejemplo, el mismo comportamiento especificado sin subtipos comunes) y con elementos globales para implementar funcionalidades comunes.

Luego (a menudo como resultado de una enseñanza demasiado entusiasta), aprenden acerca de los beneficios de la herencia, pero a menudo se enseña como la solución integral para la reutilización. Terminan con la percepción de que cualquier comportamiento compartido debe compartirse a través de la herencia. Esto se debe a que el enfoque a menudo se centra en la herencia de implementación en lugar del subtipo.

En el 80% de los casos eso es lo suficientemente bueno. Pero el otro 20% es donde comienza el problema. Con el fin de evitar la reescritura y asegurarse de que han aprovechado la implementación compartida, comienzan a diseñar su jerarquía alrededor de la implementación prevista en lugar de las abstracciones subyacentes. La "Pila hereda de la lista doblemente enlazada" es un buen ejemplo de esto.

La mayoría de los maestros no insisten en introducir el concepto de interfaces en este punto, ya sea porque es otro concepto o porque (en C ++) tienes que simularlos usando clases abstractas y herencia múltiple que no se enseña en esta etapa. En Java, muchos profesores no distinguen la "no herencia múltiple" o "la herencia múltiple es mala" de la importancia de las interfaces.

Todo esto se ve agravado por el hecho de que hemos aprendido tanto de la belleza de no tener que escribir código superfluo con herencia de implementación, que toneladas de código de delegación simple no parece natural. Así que la composición se ve desordenada, por lo que necesitamos estas reglas de oro para que los nuevos programadores las usen de todos modos (lo cual también exageran).

    
respondido por el Uri 05.04.2011 - 17:26
8

En uno de los comentarios, Mason mencionó que un día hablaríamos sobre Herencia considerada dañina .

Eso espero.

El problema con la herencia es a la vez simple y mortal, no respeta la idea de que un concepto debe tener una funcionalidad.

En la mayoría de los idiomas OO, al heredar de una clase base, usted:

  • heredar de su interfaz
  • heredará de su implementación (datos y métodos)

Y aquí se convierte el problema.

Este es no el único enfoque, aunque la mayoría de los lenguajes 00 están bloqueados. Afortunadamente, existen interfaces / clases abstractas en esos.

Además, la falta de facilidad para hacer lo contrario contribuye a que se use en gran medida: francamente, incluso sabiendo esto, ¿heredaría de una interfaz e incrustaría la clase base por composición, delegando la mayoría de las llamadas a métodos? Sin embargo, sería mucho mejor, incluso se le advertiría si de repente aparece un nuevo método en la interfaz y tendría que elegir, conscientemente, cómo implementarlo.

Como contrapunto, Haskell solo permite utilizar el principio de Liskov cuando se "deriva" de interfaces puras (denominadas conceptos) (1). No puede derivar de una clase existente, solo la composición le permite incrustar sus datos.

(1) los conceptos pueden proporcionar un valor predeterminado razonable para una implementación, pero como no tienen datos, este valor predeterminado solo se puede definir en términos de los otros métodos propuestos por el concepto o en términos de constantes.

    
respondido por el Matthieu M. 06.04.2011 - 19:14
7

La respuesta simple es: la herencia tiene mayor acoplamiento que la composición. Dadas dos opciones, de calidades equivalentes, elija la que está menos acoplada.

    
respondido por el Jeffrey Faust 05.04.2011 - 22:27
7

Creo que este tipo de consejo es como decir "Prefiero conducir a volar". Es decir, los aviones tienen todo tipo de ventajas sobre los autos, pero eso viene con cierta complejidad. Entonces, si muchas personas intentan volar desde el centro de la ciudad a los suburbios, el consejo que realmente, realmente necesitan escuchar es que no necesitan volar, y de hecho, volar simplemente lo hará Más complicado a largo plazo, incluso si parece fresco / eficiente / fácil a corto plazo. Mientras que cuando do necesita volar, generalmente se supone que es obvio.

Del mismo modo, la herencia puede hacer cosas que la composición no puede, pero debes usar eso cuando lo necesites, y no cuando no lo hagas. Entonces, si nunca está tentado de asumir que necesita herencia cuando no la necesita, entonces no necesita el consejo de "prefiera la composición". Pero muchas personas hacen , y hacen necesitan ese consejo.

Se supone que debe estar implícito en que cuando realmente necesitas la herencia, es obvio, y debes usarla entonces.

También, la respuesta de Steven Lowe. Realmente, realmente eso.

    
respondido por el Jack V. 26.11.2012 - 18:58
2

La herencia no es inherentemente mala, y la composición no es inherentemente buena. No son más que herramientas que un programador de OO puede usar para diseñar software.

Cuando miras una clase, ¿está haciendo más de lo que debería ( SRP )? ¿Está duplicando la funcionalidad innecesariamente ( DRY ), o está demasiado interesado en las propiedades o métodos de otras clases ( Feature Envy )? Si la clase está violando todos estos conceptos (y posiblemente más), ¿está intentando ser una God Class ? Estos son una serie de problemas que pueden ocurrir al diseñar software, ninguno de los cuales es necesariamente un problema de herencia, pero que puede crear rápidamente dolores de cabeza importantes y dependencias frágiles donde también se ha aplicado el polimorfismo.

Es probable que la raíz del problema sea menos una falta de comprensión sobre la herencia, y más una de las dos opciones de diseño o de no reconocer "olores de código" relacionados con las clases que no se adhieren a los Principio de Responsabilidad Única . Polimorfismo y Liskov Substitution no deben ser descartados en favor de la composición. El polimorfismo en sí mismo puede aplicarse sin depender de la herencia, todos estos son conceptos bastante complementarios. Si se aplica pensativamente. El truco es tratar de mantener su código simple y limpio, y no sucumbir a la preocupación excesiva por la cantidad de clases que necesita crear para crear un diseño de sistema robusto.

En lo que respecta al tema de favorecer la composición sobre la herencia, este es solo un caso más de la cuidadosa aplicación de los elementos de diseño que tienen más sentido en términos del problema que se resuelve. Si no necesita para heredar el comportamiento, entonces probablemente no debería, ya que la composición ayudará a evitar incompatibilidades y mayores esfuerzos de refactorización más adelante. Si, por otro lado, encuentra que está repitiendo una gran cantidad de código de tal manera que toda la duplicación se centra en un grupo de clases similares, entonces puede ser que la creación de un ancestro común ayude a reducir el número de llamadas y clases idénticas. Es posible que tengas que repetir entre cada clase. Por lo tanto, está favoreciendo la composición, pero no está asumiendo que la herencia nunca es aplicable.

    
respondido por el S.Robins 19.04.2012 - 05:29
-1

La cita real proviene, creo, de "Effective Java" de Joshua Bloch, donde es el título de uno de los capítulos. Afirma que la herencia es segura dentro de un paquete y donde se haya diseñado y documentado específicamente una clase para su extensión. Afirma, como otros han señalado, que la herencia rompe la encapsulación porque una subclase depende de los detalles de implementación de su superclase. Esto conduce, dice, a la fragilidad.

Aunque no lo menciona, me parece que la herencia única en Java significa que la composición le brinda mucha más flexibilidad que herencia.

    
respondido por el Vanessa Williams 05.04.2011 - 23:03

Lea otras preguntas en las etiquetas