over oop inheritance polymorphism composition

oop - composition over inheritance javascript



¿Hay algo que la composición no puede lograr que la herencia pueda? (5)

Considere un juego de herramientas GUI.

Un control de edición es una ventana, debe heredar las funciones de cerrar / habilitar / pintar de la ventana; no contiene una ventana.

Entonces, un control de texto enriquecido debería contener las funciones de guardar y editar / leer / cortar / pegar de los controles de edición. Sería muy difícil usarlo si solo contuviera una ventana y un control de edición.

Composición y herencia.

Soy consciente de que ambas son herramientas para elegir cuando sea apropiado, y el contexto es muy importante para elegir entre composición y herencia. Sin embargo, la discusión sobre el contexto apropiado para cada uno suele ser un poco confusa; esto me hace comenzar a considerar cuán claramente la herencia y el polimorfismo son aspectos separados de la POO tradicional.

El polimorfismo permite especificar relaciones "is-a" por igual y como herencia. Particularmente, heredar de una clase base implícitamente crea una relación polimórfica entre esa clase y sus subclases. Sin embargo, mientras que el polimorfismo se puede implementar utilizando interfaces puras, la herencia complica la relación polimórfica transfiriendo simultáneamente los detalles de implementación. De esta manera, la herencia es bastante distinta del polimorfismo puro.

Como herramienta, la herencia sirve a los programadores de forma diferente que el polimorfismo (a través de interfaces puras) al simplificar la reutilización de la implementación en casos triviales . Sin embargo, en la mayoría de los casos, los detalles de implementación de una superclase entran en conflicto sutilmente con los requisitos de una subclase. Es por eso que tenemos "reemplazos" y "miembros que se esconden". En estos casos, la reutilización de implementación ofrecida por herencia se compra con el esfuerzo adicional de verificar los cambios de estado y las rutas de ejecución a través de niveles de código en cascada: los detalles completos de implementación de la subclase se reparten entre varias clases, lo que generalmente significa archivos múltiples, de los cuales solo porciones se aplican a la subclase en cuestión. Mirar a través de esa jerarquía es absolutamente necesario cuando se trata de herencia, ya que sin mirar el código de la superclase, no hay forma de saber qué detalles sin justificación están alterando su estado o desviando su ejecución.

En comparación, el uso exclusivo de la composición garantiza que verá qué estado puede modificarse mediante objetos explícitamente instanciados cuyos métodos se invocan a su criterio. La implementación realmente plana aún no se logra (y en realidad ni siquiera es deseable, ya que el beneficio de la programación estructurada es la encapsulación y abstracción de los detalles de implementación) pero aún así obtienes la reutilización de código, y solo tendrás que mirar en un solo lugar cuando el código se comporta mal

Con el objetivo de probar estas ideas en la práctica, evitando la herencia tradicional de una combinación de polimorfismo puro basado en interfaces y composición de objetos, me pregunto:

¿Hay algo que la composición de objetos y las interfaces no puedan lograr que la herencia pueda?

Editar

En las respuestas hasta el momento, ewernli cree que no hay hazañas técnicas disponibles para una técnica, pero no para la otra; luego menciona cómo diferentes patrones y enfoques de diseño son inherentes a cada técnica. Esto es razonable. Sin embargo, la sugerencia me lleva a refinar mi pregunta al preguntar si el uso exclusivo de la composición y las interfaces en lugar de la herencia tradicional prohibiría el uso de cualquier patrón de diseño importante. Y si es así, ¿no hay patrones equivalentes para usar en mi situación?


La composición no puede arruinar la vida como lo hace la herencia, cuando los programadores de armas rápidas intentan resolver problemas al agregar métodos y extender las jerarquías (en lugar de pensar en las jerarquías naturales)

La composición no puede dar como resultado diamantes extraños, que hacen que los equipos de mantenimiento quemen aceite de la noche rascándose la cabeza

La herencia fue la esencia de la discusión en los patrones de GOF Design, que no habría sido lo mismo si los programadores usaran Composition en primer lugar.


Puedo estar equivocado sobre esto, pero lo voy a decir de todos modos, y si alguien tiene una razón por la que estoy equivocado, por favor solo responda con un comentario y no me vote. Hay una situación en la que puedo pensar donde la herencia sería superior a la composición.

Supongamos que tengo una biblioteca de widgets de fuente cerrada que estoy usando en un proyecto (lo que significa que los detalles de implementación son un misterio para mí además de lo que está documentado). Ahora supongamos que cada widget tiene la capacidad de agregar widgets secundarios. Con la herencia, podría extender la clase Widget para crear un CustomWidget, y luego agregar CustomWidget como widget infantil de cualquier otro widget en la biblioteca. Entonces mi código para agregar un CustomWidget se vería así:

Widget baseWidget = new Widget(); CustomWidget customWidget = new CustomWidget(); baseWidget.addChildWidget(customWidget);

Muy limpio, y se mantiene en línea con las convenciones de la biblioteca para agregar widgets infantiles. Sin embargo, con la composición, tendría que ser algo como esto:

Widget baseWidget = new Widget(); CustomWidget customWidget = new CustomWidget(); customWidget.addAsChildToWidget(baseWidget);

No tan limpio, y también rompe las convenciones de la biblioteca

Ahora no estoy diciendo que no puedas lograr esto con la composición (de hecho, mi ejemplo muestra que puedes hacerlo muy claramente), simplemente no es ideal en todas las circunstancias y puede llevar a romper convenciones y otras soluciones visuales poco atractivas.


Sí. Es la identificación del tipo de tiempo de ejecución (RTTI).


Técnicamente, todo lo que se puede realizar con herencia se puede realizar también con delegación. Entonces la respuesta sería "no".

Transformando herencia en delegación

Digamos que tenemos las siguientes clases implementadas con herencia:

public class A { String a = "A"; void doSomething() { .... } void getDisplayName() { return a } void printName { System.out.println( this.getDisplayName() }; } public class B extends A { String b = "B"; void getDisplayName() { return a + " " + b; } void doSomething() { super.doSomething() ; ... } }

Las cosas funcionan bien, y al llamar a printName en una instancia de B se imprimirá "AB" en la consola.

Ahora, si reescribimos eso con delegación, obtenemos:

public class A { String a = "A"; void doSomething() { .... } void getDisplayName() { return a } void printName { System.out.println( this.getName() }; } public class B { String b = "B"; A delegate = new A(); void getDisplayName() { return delegate.a + " " + b; } void doSomething() { delegate.doSomething() ; ... } void printName() { delegate.printName() ; ... } }

Necesitamos definir printName en B y también crear el delegado cuando B se crea una instancia. Una llamada a hacer algo funcionará de forma similar a como sucede con la herencia. Pero una llamada a printName imprimirá "A" en la consola. De hecho, con la delegación, hemos perdido el poderoso concepto de que "esto" está ligado a la instancia del objeto y los métodos de base pueden invocar a los métodos que deben anularse.

Esto se puede resolver en el lenguaje compatible con la delegación pura. Con la delegación pura, "esto" en el delegado seguirá haciendo referencia a la instancia de B. Lo que significa que this.getName() iniciará el envío del método desde la clase B. Logramos lo mismo que con la herencia. Este es el mecanismo utilizado en el lenguaje prototype-based en prototype-based , como Self que tiene una función incorporada (puede leer here cómo funciona la herencia en Self).

Pero Java no tiene delegación pura. ¿Cuándo está atascado? No, realmente, aún podemos hacerlo nosotros mismos con un poco más de esfuerzo:

public class A implements AInterface { String a = "A"; AInterface owner; // replace "this" A ( AInterface o ) { owner = o } void doSomething() { .... } void getDisplayName() { return a } void printName { System.out.println( owner.getName() }; } public class B implements AInterface { String b = "B"; A delegate = new A( this ); void getDisplayName() { return delegate.a + " " + b; } void doSomething() { delegate.doSomething() ; ... } void printName() { delegate.printName() ; ... } }

Básicamente, estamos volviendo a implementar lo que ofrece la herencia incorporada. ¿Tiene sentido? No realmente. Pero ilustra que la herencia siempre se puede convertir en delegación.

Discusión

La herencia se caracteriza por el hecho de que una clase base puede llamar a un método que se anula en una subclase. Esta es, por ejemplo, la esencia del patrón de la plantilla . Tales cosas no se pueden hacer fácilmente con la delegación. Por otro lado, esto es exactamente lo que hace que la herencia sea difícil de usar. Se requiere un giro mental para comprender dónde ocurre el envío polimórfico y cuál es el efecto si los métodos se anulan.

Existen algunos escollos conocidos sobre la herencia y la fragilidad que puede presentar en el diseño. Especialmente si la jerarquía de clases evoluciona. También puede haber algunos problemas con la equality en hashCode y equals si se usa la herencia. Pero, por otro lado, sigue siendo una forma muy elegante de resolver algunos problemas.

Además, incluso si la herencia puede ser reemplazada por delegación, se puede argumentar que aún logran un propósito diferente y se complementan entre sí, no transmiten la misma intención que no es capturada por la equivalencia técnica pura.

(Mi teoría es que cuando alguien comienza a hacer OO, tenemos la tentación de usar en exceso la herencia porque se percibe como una característica del lenguaje. Luego aprendemos la delegación que es un patrón / enfoque y también aprendemos a gustarle. Después de un tiempo , encontramos un equilibrio entre ambos y desarrollamos el sentido de la intuición de cuál es mejor, en cuyo caso. Bueno, como puedes ver, todavía me gustan los dos, y ambos merecen cierta precaución antes de ser presentados.

Algo de literatura

La herencia y la delegación son métodos alternativos para la definición incremental y el intercambio. Comúnmente se ha creído que la delegación proporciona un modelo más poderoso. Este documento demuestra que existe un modelo de herencia "natural" que captura todas las propiedades de la delegación. Independientemente, se demuestran ciertas restricciones sobre la capacidad de la delegación para capturar la herencia. Finalmente, se describe un nuevo marco que captura por completo la delegación y la herencia, y se exploran algunas de las ramificaciones de este modelo híbrido.

Una de las nociones más intrigantes y al mismo tiempo más problemáticas en la programación orientada a objetos es la herencia. La herencia se considera comúnmente como la característica que distingue la programación orientada a objetos de otros paradigmas de programación modernos, pero los investigadores rara vez acuerdan su significado y uso. [...]

Debido al fuerte acoplamiento de las clases y la proliferación de miembros de la clase innecesarios inducidos por la herencia, la sugerencia de utilizar la composición y la delegación en su lugar se ha convertido en algo común. La presentación de una refactorización correspondiente en la literatura puede llevar a creer que tal transformación es una tarea directa. [...]