Siempre leo que la composición es preferible a la herencia. Una publicación de blog sobre tipos diferentes , por ejemplo, aboga por el uso de la composición sobre la herencia, pero no puedo ver cómo se logra el polimorfismo.
Pero tengo la sensación de que cuando las personas dicen que prefieren la composición, en realidad quieren decir que prefieren una combinación de composición e implementación de interfaz. ¿Cómo vas a obtener el polimorfismo sin herencia?
Aquí hay un ejemplo concreto donde uso la herencia. ¿Cómo se cambiaría esto para usar la composición y qué ganaría?
Class Shape
{
string name;
public:
void getName();
virtual void draw()=0;
}
Class Circle: public Shape
{
void draw(/*draw circle*/);
}
interfaces
inheritance
composition
MustafaM
fuente
fuente
Respuestas:
El polimorfismo no necesariamente implica herencia. A menudo, la herencia se usa como un medio fácil para implementar el comportamiento polimórfico, porque es conveniente clasificar objetos de comportamiento similares como los que tienen una estructura y comportamiento raíz completamente comunes. Piense en todos esos ejemplos de códigos de automóviles y perros que ha visto a lo largo de los años.
Pero, ¿qué pasa con los objetos que no son lo mismo? Modelar un automóvil y un planeta sería muy diferente, y sin embargo, ambos podrían querer implementar el comportamiento Move ().
En realidad, básicamente respondiste tu propia pregunta cuando dijiste
"But I have a feeling that when people say prefer composition, they really mean prefer a combination of composition and interface implementation."
. El comportamiento común se puede proporcionar a través de interfaces y un compuesto de comportamiento.En cuanto a cuál es mejor, la respuesta es algo subjetiva y realmente se reduce a cómo desea que funcione su sistema, lo que tiene sentido tanto contextual como arquitectónicamente, y lo fácil que será probar y mantener.
fuente
La composición preferida no se trata solo de polimorfismo. Aunque eso es parte de esto, y tiene razón en que (al menos en lenguajes de escritura nominal) lo que la gente realmente quiere decir es "preferir una combinación de composición e implementación de interfaz". Pero, las razones para preferir la composición (en muchas circunstancias) son profundas.
El polimorfismo se trata de una cosa que se comporta de múltiples maneras. Por lo tanto, los genéricos / plantillas son una característica "polimórfica" en la medida en que permiten que una sola pieza de código varíe su comportamiento con los tipos. De hecho, este tipo de polimorfismo es realmente el mejor comportamiento y generalmente se conoce como polimorfismo paramétrico porque la variación está definida por un parámetro.
Muchos idiomas proporcionan una forma de polimorfismo llamada "sobrecarga" o polimorfismo ad hoc en el que múltiples procedimientos con el mismo nombre se definen de manera ad hoc y donde el idioma elige uno (quizás el más específico). Este es el tipo de polimorfismo menos bien comportado, ya que nada conecta el comportamiento de los dos procedimientos, excepto la convención desarrollada.
Un tercer tipo de polimorfismo es el subtipo de polimorfismo . Aquí un procedimiento definido en un tipo dado, también puede funcionar en una familia completa de "subtipos" de ese tipo. Cuando implementa una interfaz o extiende una clase, generalmente declara su intención de crear un subtipo. Los subtipos verdaderos se rigen por el Principio de sustitución de Liskov, que dice que si puede probar algo sobre todos los objetos en un supertipo, puede probarlo sobre todas las instancias en un subtipo. Sin embargo, la vida se vuelve peligrosa, ya que en lenguajes como C ++ y Java, las personas generalmente tienen suposiciones no aplicadas, y a menudo indocumentadas, sobre clases que pueden ser ciertas o no sobre sus subclases. Es decir, el código se escribe como si se pudiera demostrar más de lo que realmente es, lo que produce una gran cantidad de problemas cuando se subtipe descuidadamente.
La herencia es en realidad independiente del polimorfismo. Dada alguna cosa "T" que tiene una referencia a sí misma, la herencia ocurre cuando crea una nueva cosa "S" a partir de "T" reemplazando la referencia de "T" a sí misma con una referencia a "S". Esa definición es intencionalmente vaga, ya que la herencia puede ocurrir en muchas situaciones, pero la más común es la subclasificación de un objeto que tiene el efecto de reemplazar el
this
puntero llamado por funciones virtuales con elthis
puntero al subtipo.La herencia es peligrosa, como todas las cosas muy poderosas, la herencia tiene el poder de causar estragos. Por ejemplo, suponga que anula un método cuando hereda de alguna clase: todo está bien hasta que algún otro método de esa clase asume que el método que hereda se comporta de cierta manera, después de todo, así es como lo diseñó el autor de la clase original. . Puede protegerse parcialmente contra esto declarando que todos los métodos invocados por otro de sus métodos son privados o no virtuales (final), a menos que estén diseñados para ser anulados. Aunque esto no siempre es lo suficientemente bueno. A veces puede ver algo como esto (en pseudo Java, con suerte legible para usuarios de C ++ y C #)
crees que esto es encantador y tienes tu propia forma de hacer "cosas", pero usas la herencia para adquirir la capacidad de hacer "más cosas",
Y todo está bien y bien.
WayOfDoingUsefulThings
fue diseñado de tal manera que reemplazar un método no cambia la semántica de ningún otro ... excepto esperar, no, no lo fue. Parece que fue, perodoThings
cambió el estado mutable que importaba. Entonces, a pesar de que no llamó a ninguna función de anulación,ahora hace algo diferente de lo esperado cuando lo pasa a
MyUsefulThings
. Lo que es peor, es posible que ni siquiera sepa queWayOfDoingUsefulThings
hizo esas promesas. Tal vezdealWithStuff
proviene de la misma bibliotecaWayOfDoingUsefulThings
ygetStuff()
ni siquiera es exportada por la biblioteca (piense en las clases de amigos en C ++). Peor aún, has derrotado las comprobaciones estáticas del lenguaje sin darte cuenta:dealWithStuff
tomaste unaWayOfDoingUsefulThings
decisión justa para asegurarte de que tuviera unagetStuff()
función que se comportara de cierta manera.Usando composición
trae de vuelta la seguridad de tipo estático. En general, la composición es más fácil de usar y más segura que la herencia cuando se implementa el subtipo. También le permite anular los métodos finales, lo que significa que debe sentirse libre de declarar todo final / no virtual, excepto en las interfaces la gran mayoría de las veces.
En un mundo mejor, los idiomas insertarían automáticamente el repetitivo con una
delegation
palabra clave. La mayoría no, así que un inconveniente son las clases más grandes. Sin embargo, puede hacer que su IDE escriba la instancia de delegación por usted.Ahora, la vida no se trata solo de polimorfismo. No puede necesitar subtipo todo el tiempo. El objetivo del polimorfismo es generalmente la reutilización del código, pero no es la única forma de lograr ese objetivo. A menudo, tiene sentido usar la composición, sin polimorfismo de subtipo, como una forma de administrar la funcionalidad.
Además, la herencia conductual tiene sus usos. Es una de las ideas más poderosas en informática. Es solo que, la mayoría de las veces, las buenas aplicaciones de OOP se pueden escribir utilizando solo el uso de herencia de interfaz y composiciones. Los dos principios
son una buena guía por los motivos anteriores y no incurre en costos sustanciales.
fuente
La razón por la que la gente dice esto es que los programadores principiantes de OOP, recién salidos de sus conferencias de polimorfismo a través de la herencia, tienden a escribir clases grandes con muchos métodos polimórficos, y luego, en algún momento, terminan con un desastre imposible de mantener.
Un ejemplo típico proviene del mundo del desarrollo de juegos. Supongamos que tiene una clase base para todas sus entidades de juego: la nave espacial del jugador, los monstruos, las balas, etc. Cada tipo de entidad tiene su propia subclase. El enfoque herencia usaría algunos métodos polimórficas, por ejemplo
update_controls()
,update_physics()
,draw()
, etc., y ponerlas en práctica para cada subclase. Sin embargo, esto significa que está acoplando una funcionalidad no relacionada: es irrelevante el aspecto de un objeto para moverlo, y no necesita saber nada sobre su IA para dibujarlo. El enfoque compositivo en su lugar define varias clases base (o interfaces), por ejemploEntityBrain
(subclases implementan AI o entrada de jugador),EntityPhysics
(subclases implementan física de movimiento) yEntityPainter
(subclases se encargan del dibujo) y una clase no polimórficaEntity
que tiene una instancia de cada uno. De esta manera, puede combinar cualquier apariencia con cualquier modelo de física y cualquier IA, y dado que los mantiene separados, su código también será mucho más limpio. Además, problemas como "Quiero un monstruo que se parezca al monstruo globo en el nivel 1, pero se comporta como el payaso loco en el nivel 15" desaparecen: simplemente toma los componentes adecuados y pégalos.Tenga en cuenta que el enfoque compositivo todavía utiliza la herencia dentro de cada componente; aunque idealmente usaría solo interfaces y sus implementaciones aquí.
"Separación de preocupaciones" es la frase clave aquí: representar la física, implementar una IA y dibujar una entidad son tres preocupaciones, y combinarlas en una entidad es la cuarta. Con el enfoque compositivo, cada preocupación se modela como una clase.
fuente
MonsterVisuals balloonVisuals
yMonsterBehaviour crazyClownBehaviour
e instanciar unaMonster(balloonVisuals, crazyClownBehaviour)
, junto a losMonster(balloonVisuals, balloonBehaviour)
yMonster(crazyClownVisuals, crazyClownBehaviour)
las instancias que se crean instancias en el nivel 1 y el nivel 15El ejemplo que ha dado es uno en el que la herencia es la elección natural. No creo que nadie afirme que la composición es siempre una mejor opción que la herencia: es solo una guía que significa que a menudo es mejor ensamblar varios objetos relativamente simples que crear muchos objetos altamente especializados.
La delegación es un ejemplo de una forma de usar la composición en lugar de la herencia. La delegación le permite modificar el comportamiento de una clase sin subclasificar. Considere una clase que proporciona una conexión de red, NetStream. Puede ser natural que la subclase de NetStream implemente un protocolo de red común, por lo que puede encontrar FTPStream y HTTPStream. Pero en lugar de crear una subclase HTTPStream muy específica para un solo propósito, por ejemplo, UpdateMyWebServiceHTTPStream, a menudo es mejor usar una instancia antigua simple de HTTPStream junto con un delegado que sabe qué hacer con los datos que recibe de ese objeto. Una razón por la que es mejor es que evita la proliferación de clases que deben mantenerse pero que nunca podrá reutilizar.
fuente
Verá mucho este ciclo en el discurso del desarrollo de software:
Se descubre que alguna característica o patrón (llamémoslo "Patrón X") es útil para algún propósito en particular. Las publicaciones de blog están escritas exaltando las virtudes sobre Pattern X.
La exageración lleva a algunas personas a pensar que debes usar el Patrón X siempre que sea posible .
Otras personas se molestan al ver que se usa el Patrón X en contextos donde no es apropiado, y escriben publicaciones en el blog indicando que no siempre debe usar el Patrón X y que es perjudicial en algunos contextos.
La reacción violenta hace que algunas personas crean que el Patrón X siempre es dañino y nunca debe usarse.
Verá que este ciclo de exageración / reacción violenta ocurre con casi cualquier característica, desde
GOTO
patrones hasta SQL y NoSQL y, sí, herencia. El antídoto es considerar siempre el contexto .Tener
Circle
descender deShape
es exactamente cómo se supone que se debe usar la herencia en los idiomas OO que admiten la herencia.La regla general de "preferir la composición sobre la herencia" es realmente engañosa sin contexto. Debería preferir la herencia cuando la herencia sea más apropiada, pero prefiera la composición cuando la composición sea más apropiada. La oración está dirigida a personas en la etapa 2 del ciclo de bombo, que piensan que la herencia debe usarse en todas partes. Pero el ciclo ha avanzado, y hoy parece hacer que algunas personas piensen que la herencia es de alguna manera mala en sí misma.
Piense en ello como un martillo contra un destornillador. ¿Prefieres un destornillador sobre un martillo? La pregunta no tiene sentido. Debe usar la herramienta adecuada para el trabajo, y todo depende de la tarea que necesita realizar.
fuente