Herencia vs. Agregación [cerrado]

151

Hay dos escuelas de pensamiento sobre cómo extender, mejorar y reutilizar mejor el código en un sistema orientado a objetos:

  1. 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.

  2. 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.

Craig Walker
fuente
9
Buena pregunta, desafortunadamente no tengo suficiente tiempo ahora.
Toon Krijthe
44
Una buena respuesta es mejor que una más rápida ... Estaré viendo mi propia pregunta, así que lo votaré al menos :-P
Craig Walker

Respuestas:

181

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.

  • Si La nueva clase es más o menos como la clase original. Usar herencia. La nueva clase ahora es una subclase de la clase original.
  • Si la nueva clase debe tener la clase original. Usar agregación. La nueva clase tiene ahora la clase original como miembro.

Sin embargo, hay una gran área gris. Entonces necesitamos varios otros trucos.

  • Si hemos usado la herencia (o planeamos usarla) pero solo usamos parte de la interfaz, o nos vemos obligados a anular una gran cantidad de funcionalidades para mantener la correlación lógica. Luego tenemos un gran olor desagradable que indica que tuvimos que usar la agregación.
  • Si hemos usado la agregación (o planeamos usarla) pero descubrimos que necesitamos copiar casi toda la funcionalidad. Entonces tenemos un olor que apunta en la dirección de la herencia.

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'.

class Dog
  Eat;
  Walk;
  Bark;
  Play;
end;

Ahora necesitamos una clase 'Gato', que necesita 'Comer', 'Caminar', 'Ronronear' y 'Jugar'. Así que primero intenta extenderlo desde un perro.

class Cat is Dog
  Purr; 
end;

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.

class Cat is Dog
  Purr; 
  Bark = null;
end;

Ok, esto funciona, pero huele mal. Entonces intentemos una agregación:

class Cat
  has Dog;
  Eat = Dog.Eat;
  Walk = Dog.Walk;
  Play = Dog.Play;
  Purr;
end;

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:

class Pet
  Eat;
  Walk;
  Play;
end;

class Dog is Pet
  Bark;
end;

class Cat is Pet
  Purr;
end;

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.

Toon Krijthe
fuente
55
La "reutilización de casi toda la funcionalidad de una clase" es la única vez que prefiero la herencia. Lo que realmente me gustaría es un lenguaje que tenga la capacidad de decir fácilmente "delegar a este objeto agregado para estos métodos específicos"; eso es lo mejor de ambos mundos.
Craig Walker, el
No estoy seguro acerca de otros idiomas, pero Delphi tiene un mecanismo que permite a los miembros implementar parte de la interfaz.
Toon Krijthe
2
El objetivo C utiliza protocolos que le permiten decidir si su función es obligatoria u opcional.
cantfindaname88
1
Gran respuesta con un ejemplo práctico. Diseñaría la clase Pet con un método llamado makeSoundy dejaría que cada subclase implementara su propio tipo de sonido. Esto ayudará en situaciones en las que tenga muchas mascotas y simplemente las tenga for_each pet in the list of pets, pet.makeSound.
blongho
38

Al comienzo de GOF afirman

Favorecer la composición de objetos sobre la herencia de clases.

Esto se discute más aquí

kit de herramientas
fuente
27

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).

Harper Shelby
fuente
Además, la diferencia se resume muy bien en la pregunta a la que se supone que debes responder. Me gustaría que la gente lea primero las preguntas antes de comenzar a traducir. ¿Promueves ciegamente patrones de diseño para convertirte en miembro del club de élite?
Val
No me gusta este enfoque, se trata más de composición
Alireza Rahmani Khalili
15

Aquí está mi argumento más común:

En cualquier sistema orientado a objetos, hay dos partes en cualquier clase:

  1. 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.

  2. 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.

Craig Walker
fuente
Creo que usa una clase abstracta para definir una interfaz y luego usa la herencia directa para cada clase de implementación concreta (por lo que es bastante superficial). Cualquier agregación está bajo la implementación concreta.
orcmid el
Si necesita interfaces adicionales además de eso, devuélvalas mediante métodos en la interfaz de la interfaz abstracta de nivel principal, enjuague repetidamente.
orcmid el
¿Crees que la agregación es mejor en este escenario? Un libro es un SellingItem, A DigitalDisc es un SelliingItem codereview.stackexchange.com/questions/14077/...
LCJ
11

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.

Bill Karwin
fuente
6

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:

  1. Es probable que su elección sea la incorrecta en algún momento
  2. Cambiar esa elección es difícil una vez que lo has hecho.
  3. La herencia tiende a ser una peor opción, ya que es más restrictiva.

Por lo tanto, tiendo a elegir la agregación, incluso cuando parece haber una fuerte relación is-a.

Craig Walker
fuente
La evolución de los requisitos significa que su diseño OO inevitablemente estará equivocado. La herencia versus la agregación es solo la punta de ese iceberg. No puedes diseñar todos los futuros posibles, así que sigue la idea de XP: resuelve los requisitos de hoy y acepta que es posible que tengas que refactorizar.
Bill Karwin el
Me gusta esta línea de investigación y preguntas. Aunque preocupado por blurrung es-a, has-a y uses-a. Comienzo con las interfaces (junto con la identificación de las abstracciones implementadas que quiero). El nivel adicional de indirección es invaluable. No sigas la conclusión de tu agregación.
orcmid
Sin embargo, ese es más o menos mi punto. Tanto la herencia como la agregación son métodos para resolver el problema. Uno de esos métodos incurre en todo tipo de sanciones cuando tiene que resolver problemas mañana.
Craig Walker, el
En mi opinión, la agregación + interfaces son la alternativa a la herencia / subclasificación; no puedes hacer polimorfismo basado solo en la agregación.
Craig Walker, el
3

La pregunta normalmente se formula como Composición vs. Herencia , y se ha hecho aquí antes.

Bill el lagarto
fuente
Correcto, eres ... divertido que SO no haya dado esa como una de las preguntas relacionadas.
Craig Walker, el
2
La búsqueda de SO todavía apesta
Vinko Vrsalovic el
Convenido. Por eso no cerré esto. Eso, y mucha gente ya había respondido aquí.
Bill the Lizard
1
SO necesita una función para colocar 2 preguntas (y sus respuestas) en 1
Craig Walker
3

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).

orcmid
fuente
2

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.

Salman Kasbati
fuente
2

Creo que no es un debate de uno u otro. Es solo que:

  1. Las relaciones is-a (herencia) ocurren con menos frecuencia que las relaciones has-a (composición).
  2. La herencia es más difícil de corregir, incluso cuando es apropiado usarla, por lo que se debe tomar la debida diligencia porque puede romper la encapsulación, alentar un acoplamiento estrecho al exponer la implementación, etc.

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.

Vinko Vrsalovic
fuente
1

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

Nubes azules
fuente
0

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.

cfeduke
fuente