Hay dos escuelas de pensamiento sobre cómo extender, mejorar y reutilizar mejor el código en un sistema orientado a objetos:
Herencia: amplíe la funcionalidad de una clase creando una subclase. Anule los miembros de la superclase en las subclases para proporcionar una nueva funcionalidad. Haga que los métodos sean abstractos / virtuales para forzar a las subclases a "rellenar los espacios en blanco" cuando la superclase quiera una interfaz particular pero sea independiente de su implementación.
Agregación: cree una nueva funcionalidad tomando otras clases y combinándolas en una nueva clase. Adjunte una interfaz común a esta nueva clase para la interoperabilidad con otro código.
¿Cuáles son los beneficios, costos y consecuencias de cada uno? ¿Hay otras alternativas?
Veo que este debate surge de manera regular, pero no creo que se haya preguntado en Stack Overflow todavía (aunque hay una discusión relacionada). También hay una sorprendente falta de buenos resultados de Google para ello.
fuente
Respuestas:
No se trata de cuál es el mejor, sino de cuándo usar qué.
En los casos "normales", una simple pregunta es suficiente para averiguar si necesitamos herencia o agregación.
Sin embargo, hay una gran área gris. Entonces necesitamos varios otros trucos.
Para acortarlo. Deberíamos usar la agregación si parte de la interfaz no se usa o si tiene que cambiarse para evitar una situación ilógica. Solo necesitamos usar la herencia, si necesitamos casi toda la funcionalidad sin cambios importantes. Y en caso de duda, use la agregación.
Otra posibilidad para el caso de que tengamos una clase que necesita parte de la funcionalidad de la clase original, es dividir la clase original en una clase raíz y una subclase. Y deje que la nueva clase herede de la clase raíz. Pero debe tener cuidado con esto, no para crear una separación ilógica.
Agreguemos un ejemplo. Tenemos una clase 'Perro' con métodos: 'Comer', 'Caminar', 'Ladrar', 'Jugar'.
Ahora necesitamos una clase 'Gato', que necesita 'Comer', 'Caminar', 'Ronronear' y 'Jugar'. Así que primero intenta extenderlo desde un perro.
Parece, está bien, pero espera. Este gato puede ladrar (los amantes de los gatos me matarán por eso). Y un gato ladrando viola los principios del universo. Por lo tanto, debemos anular el método Bark para que no haga nada.
Ok, esto funciona, pero huele mal. Entonces intentemos una agregación:
Ok, esto es lindo Este gato ya no ladra, ni siquiera calla. Pero aún tiene un perro interno que quiere salir. Entonces intentemos la solución número tres:
Esto es mucho más limpio. No hay perros internos. Y los gatos y los perros están al mismo nivel. Incluso podemos introducir otras mascotas para ampliar el modelo. A menos que sea un pez, o algo que no camina. En ese caso, nuevamente necesitamos refactorizar. Pero eso es algo para otro momento.
fuente
makeSound
y dejaría que cada subclase implementara su propio tipo de sonido. Esto ayudará en situaciones en las que tenga muchas mascotas y simplemente las tengafor_each pet in the list of pets, pet.makeSound
.Al comienzo de GOF afirman
Esto se discute más aquí
fuente
La diferencia se expresa típicamente como la diferencia entre "es un" y "tiene un". La herencia, la relación "es una", se resume muy bien en el Principio de sustitución de Liskov . La agregación, la relación "tiene una", es solo eso: muestra que el objeto de agregación tiene uno de los objetos agregados.
También existen otras distinciones: la herencia privada en C ++ indica que una relación "se implementa en términos de", que también puede modelarse mediante la agregación de objetos miembro (no expuestos).
fuente
Aquí está mi argumento más común:
En cualquier sistema orientado a objetos, hay dos partes en cualquier clase:
Su interfaz : la "cara pública" del objeto. Este es el conjunto de capacidades que anuncia al resto del mundo. En muchos idiomas, el conjunto está bien definido en una "clase". Por lo general, estas son las firmas de método del objeto, aunque varía un poco según el idioma.
Su implementación : el trabajo "detrás de escena" que realiza el objeto para satisfacer su interfaz y proporcionar funcionalidad. Este es típicamente el código y los datos del miembro del objeto.
Uno de los principios fundamentales de OOP es que la implementación está encapsulada (es decir, oculta) dentro de la clase; Lo único que los extraños deberían ver es la interfaz.
Cuando una subclase hereda de una subclase, generalmente hereda tanto la implementación como la interfaz. Esto, a su vez, significa que estás obligado a aceptar ambos como restricciones en tu clase.
Con la agregación, puede elegir la implementación o la interfaz, o ambas, pero no está obligado a hacerlo. La funcionalidad de un objeto se deja al propio objeto. Puede diferir a otros objetos como quiera, pero en última instancia es responsable de sí mismo. En mi experiencia, esto lleva a un sistema más flexible: uno que es más fácil de modificar.
Entonces, cada vez que desarrollo un software orientado a objetos, casi siempre prefiero la agregación sobre la herencia.
fuente
Di una respuesta a "Es un" vs "Tiene un": ¿cuál es mejor? .
Básicamente, estoy de acuerdo con otras personas: use la herencia solo si su clase derivada realmente es el tipo que está extendiendo, no simplemente porque contiene los mismos datos. Recuerde que la herencia significa que la subclase gana los métodos y los datos.
¿Tiene sentido que su clase derivada tenga todos los métodos de la superclase? ¿O simplemente se promete en silencio que esos métodos deben ignorarse en la clase derivada? ¿O te encuentras anulando métodos de la superclase, haciéndolos no operativos para que nadie los llame inadvertidamente? ¿O dando pistas a su herramienta de generación de documentos API para omitir el método del documento?
Esas son pistas fuertes de que la agregación es la mejor opción en ese caso.
fuente
Veo muchas respuestas "is-a vs. has-a; son conceptualmente diferentes" sobre esta y las preguntas relacionadas.
Lo único que he encontrado en mi experiencia es que tratar de determinar si una relación es "es-a" o "tiene-a" está destinada al fracaso. Incluso si puede hacer correctamente esa determinación para los objetos ahora, cambiar los requisitos significa que probablemente se equivocará en algún momento en el futuro.
Otra cosa que he encontrado es que es muy difícil convertir de herencia a agregación una vez que hay mucho código escrito alrededor de una jerarquía de herencia. Simplemente cambiar de una superclase a una interfaz significa cambiar casi todas las subclases del sistema.
Y, como mencioné en otra parte de esta publicación, la agregación tiende a ser menos flexible que la herencia.
Entonces, tienes una tormenta perfecta de argumentos contra la herencia cada vez que tienes que elegir uno u otro:
Por lo tanto, tiendo a elegir la agregación, incluso cuando parece haber una fuerte relación is-a.
fuente
La pregunta normalmente se formula como Composición vs. Herencia , y se ha hecho aquí antes.
fuente
Quería hacer esto un comentario sobre la pregunta original, pero 300 caracteres muerden [; <).
Creo que debemos tener cuidado. Primero, hay más sabores que los dos ejemplos bastante específicos hechos en la pregunta.
Además, sugiero que es valioso no confundir el objetivo con el instrumento. Uno quiere asegurarse de que la técnica o metodología elegida respalde el logro del objetivo principal, pero no creo que la discusión fuera de contexto sobre cuál es la mejor técnica sea muy útil. Ayuda a conocer las trampas de los diferentes enfoques junto con sus claros puntos dulces.
Por ejemplo, ¿qué es lo que quieres lograr, qué tienes disponible para comenzar y cuáles son las limitaciones?
¿Estás creando un marco de componentes, incluso uno especial? ¿Las interfaces son separables de las implementaciones en el sistema de programación o se logra mediante una práctica que utiliza un tipo diferente de tecnología? ¿Puede separar la estructura de herencia de las interfaces (si las hay) de la estructura de herencia de las clases que las implementan? ¿Es importante ocultar la estructura de clases de una implementación del código que se basa en las interfaces que ofrece la implementación? ¿Existen múltiples implementaciones para ser utilizables al mismo tiempo o es la variación más a lo largo del tiempo como consecuencia del mantenimiento y la mejora? Esto y más deben considerarse antes de fijarse en una herramienta o metodología.
Finalmente, ¿es tan importante encerrar las distinciones en la abstracción y cómo se piensa (como en is-a versus has-a) a las diferentes características de la tecnología OO? Quizás sea así, si mantiene la estructura conceptual consistente y manejable para usted y otros. Pero es aconsejable no ser esclavizado por eso y las contorsiones que podrías terminar haciendo. Tal vez sea mejor retroceder un nivel y no ser tan rígido (pero deje una buena narración para que otros puedan decir qué pasa). [Busco lo que hace que una parte particular de un programa sea explicable, pero algunas veces busco elegancia cuando hay una victoria mayor. No siempre es la mejor idea.]
Soy un purista de la interfaz, y me atraen los tipos de problemas y enfoques en los que el purismo de la interfaz es apropiado, ya sea creando un marco de Java u organizando algunas implementaciones de COM. Eso no lo hace apropiado para todo, ni siquiera cerca de todo, aunque lo juro. (Tengo un par de proyectos que parecen proporcionar contraejemplos serios contra el purismo de la interfaz, por lo que será interesante ver cómo logro hacer frente).
fuente
Cubriré la parte donde podrían aplicarse. Aquí hay un ejemplo de ambos, en un escenario de juego. Supongamos que hay un juego que tiene diferentes tipos de soldados. Cada soldado puede tener una mochila que puede contener cosas diferentes.
Herencia aquí? Hay una boina marina, verde y un francotirador. Estos son tipos de soldados. Entonces, hay un soldado de clase base con Marine, Green Beret y Sniper como clases derivadas
¿Agregación aquí? La mochila puede contener granadas, pistolas (diferentes tipos), cuchillo, botiquín, etc. Un soldado puede equiparse con cualquiera de estos en cualquier momento dado, además de que también puede tener un chaleco antibalas que actúa como armadura cuando es atacado y su la lesión disminuye a un cierto porcentaje. La clase de soldado contiene un objeto de clase de chaleco antibalas y la clase de mochila que contiene referencias a estos artículos.
fuente
Creo que no es un debate de uno u otro. Es solo que:
Ambos tienen su lugar, pero la herencia es más riesgosa.
Aunque, por supuesto, no tendría sentido tener una clase Shape 'having-a' Point y un Square. Aquí se debe la herencia.
Las personas tienden a pensar primero en la herencia cuando intentan diseñar algo extensible, eso es lo que está mal.
fuente
El favor ocurre cuando ambos candidatos califican. A y B son opciones y usted está a favor de A. La razón es que la composición ofrece más posibilidades de extensión / flexibilidad que la generalización. Esta extensión / flexibilidad se refiere principalmente al tiempo de ejecución / flexibilidad dinámica.
El beneficio no es inmediatamente visible. Para ver el beneficio, debe esperar la próxima solicitud de cambio inesperado. Entonces, en la mayoría de los casos, aquellos que se apegaron a la generlalización fallan en comparación con aquellos que adoptaron la composición (excepto un caso obvio mencionado más adelante). De ahí la regla. Desde el punto de vista del aprendizaje, si puede implementar una inyección de dependencia con éxito, entonces debe saber cuál favorecer y cuándo. La regla también te ayuda a tomar una decisión; Si no está seguro, seleccione la composición.
Resumen: Composición: el acoplamiento se reduce con solo tener algunas cosas más pequeñas que se conectan a algo más grande, y el objeto más grande simplemente vuelve a llamar al objeto más pequeño. Generlización: desde el punto de vista de la API, definir que un método se puede anular es un compromiso más fuerte que definir que se puede llamar a un método. (muy pocas ocasiones cuando gana la generalización). Y nunca olvides que con la composición también estás usando la herencia, desde una interfaz en lugar de una gran clase
fuente
Ambos enfoques se utilizan para resolver diferentes problemas. No siempre necesita agregar más de dos o más clases cuando hereda de una clase.
A veces tiene que agregar una sola clase porque esa clase está sellada o tiene miembros no virtuales que necesita interceptar para crear una capa proxy que obviamente no es válida en términos de herencia pero siempre que la clase que está representando tiene una interfaz a la que puede suscribirse, esto puede funcionar bastante bien.
fuente