"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 derivamos Car
, Airplane
, Balloon
y Ship
.
Nota : Si necesita fundamentar este ejemplo, imagine que estos son tipos de objetos en un videojuego.
Entonces Car
y Airplane
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 entre Airplane
y Balloon
. 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 que Airplane
es una FlyingVehicule
(herencia de clase), pero podríamos decir que Airplane
tiene un Flying
componente (composición) y Airplane
es una IFlyingVehicule
(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 componer B
?
Si A
una especialización de B
eso debe cumplir el principio de sustitución de Liskov , la herencia es viable, incluso deseable. Si hay situaciones en las A
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 de B
exponer métodos que podrían dar como resultado un estado no válido A
, deberíamos usar composición en lugar de herencia. Incluso si somos el autor de ambos B
y A
, es una cosa menos de qué preocuparse, por lo tanto, la composición facilita la reutilización de B
.
Incluso podemos argumentar que si hay características B
que A
no son necesarias (y no sabemos si esas características podrían dar lugar a un estado no válido A
, 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:
La herencia expone una subclase a los detalles de la implementación de su padre, a menudo se dice que 'la herencia rompe la encapsulación'
- 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:
Diseñe para la herencia, o prohíbala.
- 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:
En el desarrollo de software, el problema del yoyo es un antipatrón que ocurre cuando un programador tiene que leer y comprender un programa cuyo gráfico de herencia es tan largo y complicado que el programador tiene que seguir cambiando entre muchas definiciones de clase diferentes para seguir El flujo de control del programa.
Puede resolver, por ejemplo: su clase C
no heredará de la clase B
. En cambio, su clase C
tendrá un miembro de tipo A
, que puede o no ser (o tener) un objeto de tipo B
. De esta manera, no estará programando contra el detalle de implementación de B
, 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.