En los últimos meses, el mantra "favorecer la composición sobre la herencia" parece haber surgido de la nada y convertirse casi en una especie de meme dentro de la comunidad de programación. Y cada vez que lo veo, estoy un poco desconcertado. Es como si alguien dijera "taladros a favor sobre 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 fuera inherentemente superior a la otra no tiene sentido.
Además, nunca veo una explicación real de por qué la herencia es mala y la composición es buena, lo que me hace sospechar más. ¿Se supone que solo debe ser aceptado por fe? La sustitución de Liskov y el polimorfismo tienen beneficios bien conocidos y claros, y la OMI comprende todo el punto de usar la programación orientada a objetos, y nadie explica por qué deberían descartarse a favor de la composición.
¿Alguien sabe de dónde viene este concepto y cuál es la razón de ser?
fuente
Respuestas:
Aunque creo que he escuchado discusiones de composición vs 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 se ha degenerado en líneas típicas:
El meme, originalmente destinado a llevar a los n00bs a la iluminación, ahora se usa como un club para golpearlos hasta dejarlos inconscientes.
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 utilizar para simular la herencia con mucho trabajo adicional , esto no convierte a la herencia en un ciudadano de segunda clase, ni hace de la composición el hijo favorito. El hecho de que muchos n00bs intenten usar la herencia como un acceso directo no invalida el mecanismo, y casi todos los n00bs aprenden del error y, por lo tanto, mejoran.
Por favor PIENSE acerca de sus diseños, y dejar de chorros consignas.
</rant>
fuente
Experiencia.
Como usted dice, son herramientas para diferentes trabajos, pero la frase surgió porque las personas no la usaban de esa manera.
La herencia es principalmente una herramienta polimórfica, pero algunas personas, para su mayor peligro, intentan usarla como una forma de reutilizar / compartir código. El razonamiento es "bueno si heredo, entonces 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 respondan de manera instintiva heredando.
fuente
No es una idea nueva, creo que en realidad se introdujo en el libro de patrones de diseño GoF, que se publicó en 1994.
El principal problema con la herencia es que es una caja blanca. Por definición, necesita conocer los detalles de implementación de la clase de la que está heredando. Con la composición, por otro lado, solo te importa la interfaz pública de la clase que estás componiendo.
Del libro de GoF:
El artículo de Wikipedia en el libro GoF tiene una introducción decente.
fuente
Para responder una parte de su pregunta, creo que este concepto apareció por primera vez en el libro GOF Design Patterns: Elements of Reusable Object-Oriented Software , que se publicó por primera vez en 1994. La frase aparece en la parte superior de la página 20, en la introducción:
Prefacio esta declaración con una breve comparación de la herencia con la composición. No dicen "nunca use la herencia".
fuente
"Composición sobre herencia" es una forma corta (y aparentemente engañosa) de decir "Cuando sienta que los datos (o el comportamiento) de una clase deben incorporarse a otra clase, siempre considere usar la composición antes de aplicar la herencia a ciegas".
¿Por qué es esto cierto? Porque la herencia crea un estrecho acoplamiento en tiempo de compilación entre las 2 clases. La composición en contraste es un acoplamiento flexible, que entre otros permite una clara separación de preocupaciones, la posibilidad de cambiar dependencias en tiempo de ejecución y una comprobabilidad de dependencia más fácil 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 subclase concreta. Le permite cambiar entre diferentes implementaciones concretas de su dependencia en tiempo de ejecución.
Por esa razón (entre otras), probablemente verá que la herencia se usa con más frecuencia en forma de implementación de interfaz o clases abstractas que la herencia de vainilla.
Un ejemplo (metafórico) podría ser:
"Tengo una clase Snake y quiero incluir como parte de esa clase lo que sucede cuando Snake muerde. Me sentiría tentado de que Snake herede una clase BiterAnimal que tenga el método Bite () y anule ese método para reflejar la mordida venenosa Pero Composition over Inheritance me advierte que debería intentar usar composición en su lugar ... En mi caso, esto podría traducirse en que Snake tenga un miembro Bite. La clase Bite podría ser abstracta (o una interfaz) con varias subclases. Esto permitiría cosas buenas como tener las subclases VenomousBite y DryBite y poder cambiar la mordida en la misma instancia de Snake a medida que la serpiente envejece. Además, manejar todos los efectos de una Mordida en su propia clase separada podría permitirme reutilizarla en esa clase Frost , porque la escarcha muerde pero no es un BiterAnimal, y así sucesivamente ... "
fuente
Algunos posibles argumentos para la composición:
La composición es un poco más
herencia de lenguaje / marco agnóstico y lo que impone / requiere / habilita diferirá entre los idiomas en términos de a qué tiene acceso la subclase / superclase y qué implicaciones de rendimiento puede tener wrt métodos virtuales, etc. La composición es bastante básica y requiere muy poco soporte de lenguaje y, por lo tanto, las implementaciones en diferentes plataformas / marcos pueden compartir patrones de composición más fácilmente.
La composición es una forma muy simple y táctil de construir objetos.
La herencia es relativamente fácil de entender, pero aún así no se demuestra tan fácilmente en la vida real. Muchos objetos en la vida real se pueden descomponer 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, algo factible pero aún más lejos de la imagen real que la composición (obviamente, este no es un muy buen ejemplo de herencia, pero el punto sigue siendo el mismo). Incluso la palabra herencia (al menos para la mayoría de los hablantes de inglés de EE. UU., Esperaría) invoca automáticamente un significado en la línea "Algo transmitido por un pariente fallecido" que tiene cierta correlación con su significado en el software, pero todavía se ajusta de manera flexible.
La composición es casi siempre más flexible.
Con la composición siempre puede elegir definir su propio comportamiento o simplemente exponer esa parte de sus partes compuestas. De esta manera, no enfrenta ninguna de las restricciones que puede imponer una jerarquía de herencia (virtual frente a no virtual, etc.)
Entonces, podría deberse a que 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 con algunos de los puntos dolorosos de la herencia.
Descargo de responsabilidad:
Obviamente no es esta calle clara / unidireccional. 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 para favorecer la composición.
fuente
Quizás haya notado que la gente dice esto en los últimos meses, pero los buenos programadores lo saben desde hace mucho más tiempo. Ciertamente lo he estado diciendo cuando sea apropiado durante aproximadamente una década.
El punto del concepto es que hay una gran sobrecarga conceptual para la herencia. Cuando usa la herencia, cada llamada de método tiene un despacho implícito. Si tiene árboles de herencia profundos, o despacho múltiple, o (aún peor) ambos, entonces averiguar dónde se enviará el método en particular en cualquier llamada en particular puede convertirse en un PITA real. Hace que el razonamiento correcto sobre el código sea más complejo y dificulta la depuración.
Permítanme dar un ejemplo simple para ilustrar. Supongamos que en el fondo de un árbol de herencia, alguien nombra un método
foo
. Entonces alguien más aparece y agregafoo
en la parte superior del árbol, pero haciendo algo diferente. (Este caso es más común con la herencia múltiple). Ahora esa persona que trabaja en la clase raíz ha roto la oscura clase secundaria 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 captarán esto, pero también hay casos en los que no es fácil escribir pruebas de esa manera).Por el contrario, cuando usa la composición, en cada llamada generalmente es 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, averiguar dónde va la llamada también puede ser problemático. Pero, por lo general, es más sencillo de resolver). Esto facilita el razonamiento al respecto. Como beneficio adicional, la composición da como resultado que los métodos estén separados entre sí. El ejemplo anterior no debería suceder allí porque la clase secundaria pasaría a algún componente oscuro, y nunca hay una pregunta sobre si la llamada a
foo
estaba destinada al componente oscuro o al objeto principal.Ahora tiene toda la razón en 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á haciendo diría que nunca debes usar la herencia. Pero asegúrese de que sea lo correcto.
Desafortunadamente, muchos desarrolladores aprenden sobre software orientado a objetos, aprenden sobre 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. Esperemos que aprendan mejor a tiempo, pero con frecuencia esto no sucede hasta después de quitar algunas extremidades, etc. Decirles por adelantado que es una mala idea tiende a acelerar el proceso de aprendizaje y reducir las lesiones.
fuente
Es una reacción a la observación de que los principiantes de OO tienden a usar la herencia cuando no la necesitan. La herencia ciertamente no es algo malo, pero se puede usar 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 (vea el principio de Sustitución de Liskov para los detalles sangrientos), ya que cada vez que use una Base tendría sentido usar un Derivado. Otorga acceso derivado a los miembros protegidos y las funciones miembro de Base. Es una relación estrecha, lo que significa que tiene un alto acoplamiento, y es más probable que los cambios en uno requieran cambios en el otro.
El acoplamiento es algo malo. 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 hará el trabajo de manera efectiva, y la herencia lo hará, elija la herencia, porque tiene que hacerlo.
fuente
Aquí están mis dos centavos (más allá de todos los excelentes puntos ya planteados):
En mi humilde opinión, todo se reduce al hecho de que la mayoría de los programadores realmente no obtienen la herencia, y terminan exagerando, en parte como resultado de cómo se enseña este concepto. Este concepto existe como una forma de tratar de disuadirlos de exagerar, en lugar de enfocarse en enseñarles cómo hacerlo correctamente.
Cualquiera que haya pasado tiempo enseñando o asesorando 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. Por lo tanto, terminan creando tipos con superposiciones de interfaz (por ejemplo, el mismo comportamiento especificado sin subtipo común) y con globales para implementar piezas comunes de funcionalidad.
Luego (a menudo como resultado de una enseñanza demasiado entusiasta), aprenden sobre los beneficios de la herencia, pero a menudo se les enseña como la solución general para la reutilización. Terminan con la percepción de que cualquier comportamiento compartido debe ser compartido a través de la herencia. Esto se debe a que a menudo se centra en la herencia de implementación en lugar de subtipar.
En el 80% de los casos eso es lo suficientemente bueno. Pero el otro 20% es donde comienza el problema. En aras de evitar la reescritura y para asegurarse de que han aprovechado la implementación compartida, comienzan a diseñar su jerarquía en torno a la implementación prevista en lugar de las abstracciones subyacentes. La "pila hereda de una lista doblemente vinculada" es un buen ejemplo de esto.
La mayoría de los maestros no insisten en introducir el concepto de interfaces en este punto, porque es otro concepto o porque (en C ++) hay que simularlos usando clases abstractas y herencia múltiple que no se enseña en esta etapa. En Java, muchos maestros no distinguen la "herencia múltiple no" 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 la herencia de implementación, que toneladas de código de delegación simple no parece natural. Por lo tanto, la composición parece desordenada, por lo que necesitamos estas reglas generales para presionar a los nuevos programadores a usarlas de todos modos (lo cual también exageran).
fuente
En uno de los comentarios, Mason mencionó que un día estaríamos hablando de herencia considerada dañina .
Yo espero que sí.
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, cuando hereda de una clase base usted:
Y aquí se convierten en el problema.
Este no es el único enfoque, aunque los 00 idiomas están atascados en su mayoría. Afortunadamente, existen interfaces / clases abstractas en ellas.
Además, la falta de facilidad para hacer lo contrario contribuye a hacer que se use en gran medida: francamente, incluso sabiendo esto, ¿heredaría de una interfaz e integraría la clase base por composición, delegando la mayoría de las llamadas a métodos? Sin embargo, sería considerablemente 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 permita usar el Principio de Liskov cuando "derive" de interfaces puras (llamadas 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 puede definirse en términos de los otros métodos propuestos por el concepto o en términos de constantes.
fuente
La respuesta simple es: la herencia tiene un mayor acoplamiento que la composición. Dadas dos opciones, de cualidades equivalentes, elija la que esté menos acoplada.
fuente
Creo que este tipo de consejo es como decir "prefiera conducir a volar". Es decir, los aviones tienen todo tipo de ventajas sobre los automóviles, pero eso conlleva una cierta complejidad. Entonces, si muchas personas intentan volar desde el centro de la ciudad hasta los suburbios, el consejo que realmente necesitan escuchar es que no necesitan volar, y de hecho, volar lo hará más complicado a largo plazo, incluso si parece genial / eficiente / fácil a corto plazo. Mientras que cuando usted no necesita volar, se supone generalmente que es obvio.
Del mismo modo, la herencia puede hacer cosas que la composición no puede, pero debe usarla cuando la necesite y no cuando no. Entonces, si nunca está tentado a asumir que necesita herencia cuando no la necesita, entonces no necesita el consejo de "preferir composición". Sin embargo, muchas personas lo hacen , y lo hacen necesario ese consejo.
Se supone que está implícito que cuando realmente necesitas herencia, es obvio, y debes usarla entonces.
Además, la respuesta de Steven Lowe. Realmente, realmente eso.
fuente
La herencia no es inherentemente mala, y la composición no es inherentemente buena. Son simplemente 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á tratando de ser una Clase de Dios ? 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 grandes dolores de cabeza y dependencias frágiles donde también se ha aplicado el polimorfismo.
Es probable que la raíz del problema sea menos la falta de comprensión sobre la herencia, y más bien una mala elección en términos de diseño, o quizás no reconocer "olores de código" relacionados con clases que no se adhieren al Principio de Responsabilidad Única . El polimorfismo y la sustitución de Liskov no necesitan descartarse a 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 cuidadosamente. El truco es tratar de mantener su código simple y limpio, y no sucumbir a estar demasiado preocupado por la cantidad de clases que necesita crear para crear un diseño de sistema robusto.
En cuanto a la cuestión de favorecer la composición sobre la herencia, este es realmente otro caso de la aplicación reflexiva de los elementos de diseño que tienen más sentido en términos del problema que se está resolviendo. Si no necesita heredar el comportamiento, entonces probablemente no debería, ya que la composición ayudará a evitar incompatibilidades y esfuerzos de refactorización más adelante. Si, por otro lado, descubre que está repitiendo una gran cantidad de código de modo que toda la duplicación se centre en un grupo de clases similares, entonces puede ser que crear un ancestro común ayude a reducir la cantidad de llamadas y clases idénticas Es posible que deba repetir entre cada clase. Por lo tanto, estás favoreciendo la composición, pero no estás asumiendo que la herencia nunca sea aplicable.
fuente
La cita real proviene, creo, del "Java efectivo" 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 sea que una clase haya sido específicamente diseñada y documentada 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 lleva, dice, a la fragilidad.
Aunque no lo menciona, me parece que la herencia única en Java significa que la composición le da mucha más flexibilidad que la herencia.
fuente