Esta página aboga por la composición sobre la herencia con el siguiente argumento (reformulado en mis palabras):
Un cambio en la firma de un método de la superclase (que no se ha anulado en la subclase) provoca cambios adicionales en muchos lugares cuando usamos Herencia. Sin embargo, cuando usamos Composición, el cambio adicional requerido es solo en un solo lugar: la subclase.
¿Es esa realmente la única razón para favorecer la composición sobre la herencia? Porque si ese es el caso, este problema se puede aliviar fácilmente aplicando un estilo de codificación que aboga por anular todos los métodos de la superclase, incluso si la subclase no cambia la implementación (es decir, colocando anulaciones ficticias en la subclase). ¿Me estoy perdiendo de algo?
Respuestas:
Me gustan las analogías, así que aquí hay una: ¿Alguna vez has visto uno de esos televisores que tenían un reproductor de VCR incorporado? ¿Qué tal el que tiene una videograbadora y un reproductor de DVD? O uno que tenga un Blu-ray, DVD y VCR. Ah, y ahora todo el mundo está transmitiendo, por lo que necesitamos diseñar un nuevo televisor ...
Lo más probable es que si posee un televisor, no tenga uno como el anterior. Probablemente la mayoría de ellos nunca han existido. Lo más probable es que tenga un monitor o conjunto que tenga numerosas entradas para que no necesite un nuevo conjunto cada vez que haya un nuevo tipo de dispositivo de entrada.
La herencia es como el televisor con la videograbadora incorporada. Si tiene 3 tipos diferentes de comportamientos que desea usar juntos y cada uno tiene 2 opciones de implementación, necesita 8 clases diferentes para representarlas. A medida que agrega más opciones, los números explotan. Si usa composición en su lugar, evita ese problema combinatorio y el diseño tenderá a ser más extensible.
fuente
No, no es. En mi experiencia, la composición sobre la herencia es el resultado de pensar en su software como un grupo de objetos con comportamientos que se comunican entre sí / objetos que se prestan servicios entre sí en lugar de clases y datos .
De esta manera, tiene algunos objetos que proporcionan servicios. Los usa como bloques de construcción (realmente como Lego) para habilitar otro comportamiento (por ejemplo, un UserRegistrationService utiliza los servicios proporcionados por un EmailerService y un UserRepository ).
Al construir sistemas más grandes con este enfoque, naturalmente utilicé la herencia en muy pocos casos. Al final del día, la herencia es una herramienta muy poderosa que debe usarse con precaución y solo cuando sea apropiado.
fuente
"Preferir composición sobre herencia" es solo una buena heurística
Debe considerar el contexto, ya que esta no es una regla universal. No lo tome en el sentido de que nunca use la herencia cuando pueda hacer composición. Si ese fuera el caso, lo solucionaría prohibiendo la herencia.
Espero aclarar este punto a lo largo de esta publicación.
No intentaré defender los méritos de la composición por sí mismo. Lo que considero fuera de tema. En cambio, hablaré sobre algunas situaciones en las que el desarrollador puede considerar la herencia que sería mejor usando la composición. En ese sentido, la herencia tiene sus propios méritos, que también considero fuera de tema.
Ejemplo de vehículo
Estoy escribiendo sobre desarrolladores que intentan hacer las cosas tontamente, con fines narrativos.
Vayamos para una variante de los ejemplos clásicos que algunos cursos de programación orientada a objetos utilizan ... Tenemos una
Vehicle
clase, entonces derivamosCar
,Airplane
,Balloon
yShip
.Nota : Si necesita fundamentar este ejemplo, imagine que estos son tipos de objetos en un videojuego.
Entonces
Car
yAirplane
puede tener algún código común, porque ambos pueden rodar sobre ruedas en tierra. Los desarrolladores pueden considerar crear una clase intermedia en la cadena de herencia para eso. Sin embargo, en realidad también hay un código compartido entreAirplane
yBalloon
. Podrían considerar crear otra clase intermedia en la cadena de herencia para eso.Por lo tanto, el desarrollador estaría buscando herencia múltiple. En el punto donde los desarrolladores buscan herencia múltiple, el diseño ya salió mal.
Es mejor modelar este comportamiento como interfaces y composición, para que podamos reutilizarlo sin tener que encontrar una herencia de clases múltiples. Si los desarrolladores, por ejemplo, crean la
FlyingVehicule
clase. Dirían queAirplane
es unaFlyingVehicule
(herencia de clase), pero podríamos decir queAirplane
tiene unFlying
componente (composición) yAirplane
es unaIFlyingVehicule
(herencia de interfaz).Usando interfaces, si es necesario, podemos tener herencia múltiple (de interfaces). Además, no se está acoplando a una implementación particular. Aumento de la reutilización y la capacidad de prueba de su código.
Recuerde que la herencia es una herramienta para el polimorfismo. Además, el polimorfismo es una herramienta para la reutilización. Si puede aumentar la reutilización de su código utilizando la composición, hágalo. Si no está seguro de lo que sea o no, la composición proporciona una mejor reutilización, "Preferir composición sobre herencia" es una buena heurística.
Todo eso sin mencionarlo
Amphibious
.De hecho, es posible que no necesitemos cosas que despegan. Stephen Hurn tiene un ejemplo más elocuente en sus artículos "Favorecer la composición sobre la herencia", parte 1 y parte 2 .
Sustituibilidad y encapsulación
¿Debe
A
heredar o componerB
?Si
A
una especialización deB
eso debe cumplir el principio de sustitución de Liskov , la herencia es viable, incluso deseable. Si hay situaciones en lasA
que no hay una sustitución válida,B
entonces no deberíamos usar la herencia.Podríamos estar interesados en la composición como una forma de programación defensiva, para defender la clase derivada . En particular, una vez que comience a usarlo
B
para otros fines diferentes, puede haber presión para cambiarlo o extenderlo para que sea más adecuado para esos fines. Si existe el riesgo deB
exponer métodos que podrían dar como resultado un estado no válidoA
, deberíamos usar composición en lugar de herencia. Incluso si somos el autor de ambosB
yA
, es una cosa menos de qué preocuparse, por lo tanto, la composición facilita la reutilización deB
.Incluso podemos argumentar que si hay características
B
queA
no son necesarias (y no sabemos si esas características podrían dar lugar a un estado no válidoA
, ya sea en la implementación actual o en el futuro), es una buena idea usar composición en lugar de herencia.La composición también tiene las ventajas de permitir implementaciones de conmutación y facilitar la burla.
Nota : hay situaciones en las que queremos usar la composición a pesar de que la sustitución sea válida. Archivamos esa sustituibilidad mediante el uso de interfaces o clases abstractas (cuál usar cuando es otro tema) y luego usamos la composición con inyección de dependencia de la implementación real.
Finalmente, por supuesto, existe el argumento de que deberíamos usar la composición para defender la clase padre porque la herencia rompe la encapsulación de la clase padre:
- Patrones de diseño: elementos de software orientado a objetos reutilizables, pandilla de cuatro
Bueno, esa es una clase para padres mal diseñada. Por eso deberías:
- Efectivo Java, Josh Bloch
Problema de YoYo
Otro caso donde la composición ayuda es el problema de YoYo . Esta es una cita de Wikipedia:
Puede resolver, por ejemplo: su clase
C
no heredará de la claseB
. En cambio, su claseC
tendrá un miembro de tipoA
, que puede o no ser (o tener) un objeto de tipoB
. De esta manera, no estará programando contra el detalle de implementación deB
, sino contra el contrato que ofrece la interfaz (de)A
.Ejemplos de contadores
Muchos marcos favorecen la herencia sobre la composición (que es lo contrario de lo que hemos estado discutiendo). El desarrollador puede hacer esto porque ponen mucho trabajo en su clase base de que tenerlo implementado con composición hubiera aumentado el tamaño del código del cliente. A veces esto se debe a limitaciones del idioma.
Por ejemplo, un marco PHP ORM puede crear una clase base que use métodos mágicos para permitir escribir código como si el objeto tuviera propiedades reales. En cambio, el código manejado por los métodos mágicos irá a la base de datos, consultará el campo solicitado en particular (quizás lo almacene en caché para futuras solicitudes) y lo devolverá. Hacerlo con composición requeriría que el cliente cree propiedades para cada campo o escriba alguna versión del código de métodos mágicos.
Anexo : Hay algunas otras formas en que uno podría permitir extender los objetos ORM. Por lo tanto, no creo que la herencia sea necesaria en este caso. Es más barato.
Para otro ejemplo, un motor de videojuego puede crear una clase base que usa código nativo dependiendo de la plataforma de destino para hacer renderizado 3D y manejo de eventos. Este código es complejo y específico de la plataforma. Sería costoso y propenso a errores para el usuario desarrollador del motor lidiar con este código, de hecho, eso es parte de la razón del uso del motor.
Además, sin la parte de renderizado 3D, así es como funcionan muchos marcos de widgets. Esto lo libera de la preocupación de manejar los mensajes del sistema operativo ... de hecho, en muchos idiomas no puede escribir dicho código sin alguna forma de oferta nativa. Además, si lo hiciera, reduciría su portabilidad. En cambio, con herencia, siempre que el desarrollador no rompa la compatibilidad (demasiado); podrá transferir fácilmente su código a cualquier plataforma nueva que admitan en el futuro.
Además, tenga en cuenta que muchas veces solo queremos anular algunos métodos y dejar todo lo demás con las implementaciones predeterminadas. Si hubiéramos estado usando la composición, tendríamos que crear todos esos métodos, aunque solo fuera para delegar al objeto envuelto.
Según este argumento, hay un punto en el que la composición puede ser peor para la mantenibilidad que la herencia (cuando la clase base es demasiado compleja). Sin embargo, recuerde que la capacidad de mantenimiento de la herencia puede ser peor que la de la composición (cuando el árbol de herencia es demasiado complejo), que es a lo que me refiero en el problema del yoyo.
En los ejemplos presentados, los desarrolladores rara vez tienen la intención de reutilizar el código generado por herencia en otros proyectos. Eso mitiga la reutilización disminuida del uso de la herencia en lugar de la composición. Además, al usar la herencia, los desarrolladores del marco pueden proporcionar mucho código fácil de usar y fácil de descubrir.
Pensamientos finales
Como puede ver, la composición tiene alguna ventaja sobre la herencia en algunas situaciones, no siempre. Es importante tener en cuenta el contexto y los diferentes factores involucrados (como la reutilización, la mantenibilidad, la capacidad de prueba, etc.) para tomar la decisión. Volver al primer punto: "Prefiero la composición sobre la herencia" es un solo buena heurística.
También puede tener avisos de que muchas de las situaciones que describo pueden resolverse con Rasgos o Mixins hasta cierto punto. Lamentablemente, estas no son características comunes en la gran lista de idiomas, y generalmente tienen un costo de rendimiento. Afortunadamente, sus métodos de extensión primos populares y las implementaciones predeterminadas mitigan algunas situaciones.
Tengo una publicación reciente donde hablo sobre algunas de las ventajas de las interfaces en por qué requerimos interfaces entre UI, Business y acceso a datos en C # . Ayuda a desacoplar y facilita la reutilización y la capacidad de prueba, puede interesarle.
fuente
Normalmente, la idea principal de la Composición sobre herencia es proporcionar una mayor flexibilidad de diseño y no reducir la cantidad de código que necesita cambiar para propagar un cambio (que me parece cuestionable).
El ejemplo en wikipedia da un mejor ejemplo del poder de su enfoque:
Al definir una interfaz base para cada "pieza de composición", podría crear fácilmente varios "objetos compuestos" con una variedad que podría ser difícil de alcanzar solo con la herencia.
fuente
La línea: "Prefiere la composición sobre la herencia" no es más que un empujón en la dirección correcta. No te salvará de tu propia estupidez y, de hecho, te dará la oportunidad de empeorar las cosas. Eso es porque hay mucho poder aquí.
La razón principal por la que esto es necesario decir es porque los programadores son flojos. Tan perezosos tomarán cualquier solución que signifique menos tipeo del teclado. Ese es el principal punto de venta de la herencia. Escribo menos hoy y alguien más se jode mañana. Y qué, quiero irme a casa.
Al día siguiente, el jefe insiste en que use composición. No explica por qué, pero insiste en ello. Así que tomo una instancia de lo que estaba heredando, expongo una interfaz idéntica a la que estaba heredando e implemento esa interfaz delegando todo el trabajo a esa cosa de la que heredé. Todo se trata de escribir con muerte cerebral que podría haberse hecho con una herramienta de refactorización y me parece inútil, pero el jefe lo quería, así que lo hago.
Al día siguiente le pregunto si esto es lo que quería. ¿Qué crees que dice el jefe?
A menos que hubiera alguna necesidad de poder cambiar dinámicamente (en tiempo de ejecución) lo que se heredaba (ver patrón de estado), esto era una gran pérdida de tiempo. ¿Cómo puede el jefe decir eso y aún estar a favor de la composición sobre la herencia?
Además de esto, no hacer nada para evitar la rotura debido a cambios en la firma del método, esto pierde por completo la mayor ventaja de la composición. A diferencia de la herencia, eres libre de hacer una interfaz diferente . Uno más apropiado para cómo se usará en este nivel. Ya sabes, abstracción!
Hay algunas ventajas menores para reemplazar automáticamente la herencia con composición y delegación (y algunas desventajas), pero mantener el cerebro apagado durante este proceso pierde una gran oportunidad.
La indirecta es muy poderosa. Úsalo con sabiduría.
fuente
Aquí hay otra razón: en muchos lenguajes OO (estoy pensando en otros como C ++, C # o Java aquí), no hay forma de cambiar la clase de un objeto una vez que lo ha creado.
Supongamos que estamos creando una base de datos de empleados. Tenemos una clase abstracta de Empleado base, y comenzamos a derivar nuevas clases para roles específicos: ingeniero, guardia de seguridad, conductor de camión, etc. Cada clase tiene un comportamiento especializado para ese rol. Todo es bueno.
Entonces, un día un ingeniero es ascendido a gerente de ingeniería. No puede volver a clasificar a un ingeniero existente. En su lugar, debe escribir una función que cree un Gerente de ingeniería, que copie todos los datos del Ingeniero, elimine al Ingeniero de la base de datos y luego agregue el nuevo Gerente de ingeniería. Y tiene que escribir dicha función para cada posible cambio de rol.
Peor aún, supongamos que un conductor de camión se va de licencia por enfermedad a largo plazo, y un guardia de seguridad le ofrece hacer una conducción de camión a tiempo parcial para completar. Ahora tiene que inventar una nueva clase de guardia de seguridad y conductor de camión solo para ellos.
Hace la vida mucho más fácil crear una clase de empleado concreta y tratarla como un contenedor al que puede agregar propiedades, como el rol de trabajo.
fuente
La herencia proporciona dos cosas:
1) Reutilización de código: se puede lograr fácilmente a través de la composición. Y, de hecho, se logra mejor a través de la composición, ya que conserva mejor la encapsulación.
2) Polimorfismo: se puede lograr a través de "interfaces" (clases donde todos los métodos son puramente virtuales).
Un argumento sólido para usar la herencia sin interfaz es cuando la cantidad de métodos requeridos es grande y su subclase solo quiere alterar un pequeño subconjunto de ellos. Sin embargo, en general, su interfaz debe tener huellas muy pequeñas si se suscribe al principio de "responsabilidad única" (debe hacerlo), por lo que estos casos deberían ser raros.
Tener el estilo de codificación que requiere anular todos los métodos de clase principal no evita escribir una gran cantidad de métodos de transferencia de todos modos, entonces, ¿por qué no reutilizar el código a través de la composición y obtener el beneficio adicional de la encapsulación? Además, si la superclase se extendiera agregando otro método, el estilo de codificación requeriría agregar ese método a todas las subclases (y hay una buena posibilidad de que estemos violando la "segregación de interfaz" ). Este problema crece rápidamente cuando comienza a tener múltiples niveles de herencia.
Finalmente, en muchos idiomas, la prueba unitaria de objetos basados en "composición" es mucho más fácil que la prueba unitaria de objetos basados en "herencia" al sustituir el objeto miembro con un objeto simulado / prueba / ficticio.
fuente
No
Hay muchas razones para favorecer la composición sobre la herencia. No hay una sola respuesta correcta. Pero pondré otra razón para favorecer la composición sobre la herencia en la mezcla:
Favorecer la composición sobre la herencia mejora la legibilidad de su código , lo cual es muy importante cuando se trabaja en un proyecto grande con varias personas.
Así es como lo pienso: cuando leo el código de otra persona, si veo que han extendido una clase, voy a preguntar qué comportamiento en la superclase están cambiando.
Y luego revisaré su código, buscando una función de reemplazo. Si no veo ninguno, entonces lo clasifico como un mal uso de la herencia. ¡Deberían haber usado composición en su lugar, aunque solo fuera para salvarme (y a todas las demás personas del equipo) de 5 minutos de leer su código!
Aquí hay un ejemplo concreto: en Java, la
JFrame
clase representa una ventana. Para crear una ventana, crea una instancia deJFrame
y luego llama a funciones en esa instancia para agregarle cosas y mostrarla.Muchos tutoriales (incluso los oficiales) recomiendan que extiendas
JFrame
para que puedas tratar las instancias de tu clase como instancias deJFrame
. Pero en mi humilde opinión (y la opinión de muchos otros) ¡es un mal hábito! En su lugar, solo debe crear una instanciaJFrame
y llamar a funciones en esa instancia. No hay absolutamente ninguna necesidad de extenderJFrame
en esta instancia, ya que no está cambiando ninguno de los comportamientos predeterminados de la clase principal.Por otro lado, una forma de realizar una pintura personalizada es extender la
JPanel
clase y anular lapaintComponent()
función. Este es un buen uso de la herencia. Si estoy revisando su código y veo que ha extendidoJPanel
y anulado lapaintComponent()
función, sabré exactamente qué comportamiento está cambiando.Si solo está escribiendo código usted mismo, entonces podría ser tan fácil usar la herencia como usar la composición. Pero imagina que estás en un entorno grupal y que otras personas leerán tu código. Favorecer la composición sobre la herencia hace que su código sea más fácil de leer.
fuente
new
palabra clave te da uno. De todos modos, gracias por la discusión.