2008-11-06 18 views
123

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

  1. Herencia: ampliar la funcionalidad de una clase mediante la creación de una subclase . Anule los miembros de la superclase en las subclases para proporcionar una nueva funcionalidad. Cree métodos abstractos/virtuales para forzar a las subclases a "rellenar espacios en blanco" cuando la superclase desea una interfaz en particular, pero es independiente de su implementación.

  2. Agregación: cree nuevas funcionalidades 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, los costos y las consecuencias de cada uno? ¿Hay otras alternativas?

Veo este debate surgir de forma regular, pero no creo que se haya preguntado en Stack Overflow aún (aunque hay algunas discusiones relacionadas). También hay una sorprendente falta de buenos resultados de Google para ello.

+5

Buena pregunta, desafortunadamente no tengo suficiente tiempo ahora. –

+3

Una buena respuesta es mejor que una más rápida ... Voy a ver mi propia pregunta, así que te votaré al menos :-P –

+1

Se ha expandido ahora ;-). –

Respuesta

146

No es una cuestión de cuál es el mejor, sino de cuándo utilizar qué.

En los casos "normales" una simple pregunta es suficiente para saber si necesitamos herencia o agregación.

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

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

  • Si hemos utilizado la herencia (o tenemos la intención de usarlo) pero sólo utilizar parte de la interfaz, o estamos obligados a anular un montón de funcionalidades para mantener la correlación lógica. Entonces tenemos un gran olor desagradable que indica que tuvimos que usar la agregación.
  • Si usamos 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 abreviar. Deberíamos usar la agregación si parte de la interfaz no se usa o debe modificarse para evitar una situación ilógica. Solo necesitamos usar herencia, si necesitamos casi toda la funcionalidad sin grandes cambios. Y cuando tenga dudas, use Agregación.

Otra posibilidad para, si tenemos 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 deberías cuidarte con esto, no para crear una separación ilógica.

Permite agregar un ejemplo. Tenemos una clase 'Perro' con métodos: 'comer', 'caminar', 'ladrar', 'jugar'.

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

Ahora necesitamos un clase 'Gato', que necesita 'Coma', 'Walk', 'Purr', y 'Play'. Entonces primero intenta extenderlo desde un perro.

class Cat is Dog 
    Purr; 
end; 

Parece, 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. Entonces debemos anular el método de Bark para que no haga nada.

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

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

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

Ok, esto es bueno. Este gato ya no ladra, ni siquiera en silencio. Pero todavía tiene un perro interno que quiere salir. Así que vamos a probar 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. Sin perros internos. Y los gatos y los perros están en el mismo nivel. Incluso podemos presentar otras mascotas para ampliar el modelo. A menos que sea un pez, o algo que no camine. En ese caso, nuevamente tenemos que refactorizar. Pero eso es algo para otro momento.

+4

La "reutilización de casi toda la funcionalidad de una clase" es la única vez que realmente prefiero la herencia. Lo que realmente me gustaría es un lenguaje que tenga la capacidad de decir fácilmente "delegar en este objeto agregado para estos métodos específicos"; eso es lo mejor de ambos mundos. –

+0

No estoy seguro acerca de otros idiomas, pero Delphi tiene un mecanismo que permite a los miembros implementar parte de la interfaz. –

+2

Objective C usa protocolos que le permiten decidir si su función es obligatoria u opcional. – cantfindaname88

26

La diferencia se expresa generalmente como la diferencia entre "es un" y "tiene un". Herencia, la relación "es una" se resume muy bien en el Liskov Substitution Principle. La agregación, la relación "tiene una", es solo eso: muestra que el objeto que se agrega tiene uno de los objetos agregados.

Existen otras distinciones también: la herencia privada en C++ indica una relación "se implementa en términos de", que también se puede modelar mediante la agregación de objetos miembros (no expuestos) también.

+0

Además, la diferencia se resume muy bien en la misma pregunta a la que se supone que debes responder. Me gustaría que las personas primero lean las preguntas antes de comenzar a traducir. ¿Promuevo ciegamente patrones de diseño para convertirme en miembro del club de élite? – Val

33

Al comienzo de GOF Afirman

composición objeto Favor sobre la herencia de clases.

Esto se discute más here

1

Cubriré la parte de dónde podrían aplicar. 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 & un francotirador. Estos son tipos de soldados. Entonces, hay una clase base Soldado con marine, boina verde & Sniper como clases derivadas

Agregación aquí? La mochila puede contener granadas, pistolas (diferentes tipos), cuchillo, medikit, 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 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.

0

Ambos enfoques se usan 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 que cree una capa de proxy que obviamente no es válida en términos de herencia, siempre y cuando la La clase a la que está transmitiendo tiene una interfaz a la que puede suscribirse y puede funcionar bastante bien.

2

Creo que no se trata de un debate. Es solo eso:

  1. is-a (herencia) las relaciones se producen 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 ya que puede romper la encapsulación, alentar un acoplamiento estricto al exponer la implementación y demás.

Ambos tienen su lugar, pero la herencia es más arriesgada.

Aunque, por supuesto, no tendría sentido tener una clase Shape 'having-a' Point y Square classes. Aquí la herencia se debe.

Las personas tienden a pensar en la herencia primero cuando intentan diseñar algo extensible, eso es lo que está mal.

3

La pregunta normalmente se expresa como Composition vs. Inheritance y se ha preguntado aquí anteriormente.

+0

Correcto eres ..Es gracioso que SO no haya dado esa como una de las preguntas relacionadas. –

+2

SO la búsqueda todavía apesta a lo grande –

+0

De acuerdo. Es por eso que no cerré esto. Eso, y mucha gente ya había respondido aquí. –

14

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

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

  1. Su interfaz: la "cara pública" del objeto. Este es el conjunto de capacidades que anuncia para el 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 hace el objeto para satisfacer su interfaz y proporcionar funcionalidad. Este es típicamente el código y los datos de los miembros del objeto.

Uno de los principios fundamentales de la programación orientada a objetos es que la aplicación es encapsulado (es decir: oculto) dentro de la clase; lo único que deberían ver los de afuera es la interfaz.

Cuando una subclase hereda de una subclase, normalmente hereda ambos la implementación y la interfaz. Esto, a su vez, significa que usted es forzado para aceptar ambos como restricciones en su clase.

Con la agregación, puede elegir la implementación o la interfaz, o ambas cosas, pero no está obligado a hacerlo. La funcionalidad de un objeto se deja al objeto mismo. Puede diferir a otros objetos como quiera, pero en última instancia es responsable de sí mismo. En mi experiencia, esto conduce a un sistema más flexible: uno que es más fácil de modificar.

Por lo tanto, cada vez que desarrollo software orientado a objetos, casi siempre prefiero la agregación sobre la herencia.

+0

Creo que utilizas una clase abstracta para definir una interfaz y luego utilizas la herencia directa para cada clase concreta de implementación (lo que hace que sea bastante superficial). Cualquier agregación está bajo la implementación concreta. – orcmid

+0

Si necesita interfaces adicionales, devuélvalas a través de métodos en la interfaz de la interfaz abstraída de nivel principal, repetición de enjuague. – orcmid

+0

¿Cree que la agregación es mejor en este escenario? Un libro es un artículo vendedor, un DigitalDisc es un elemento Selliing http://codereview.stackexchange.com/questions/14077/is-it-proper-tpt-inheritance – Lijo

11

He dado una respuesta a "Is a" vs "Has a" : which one is better?.

Básicamente estoy de acuerdo con otras personas: utilizar la herencia sólo si su clase derivada verdaderamente es el tipo que está extendiéndose, no sólo porque contiene los mismos datos. Recuerde que la herencia significa que la subclase gana tanto los métodos como los datos.

¿Tiene sentido que su clase derivada tenga todos los métodos de la superclase? ¿O simplemente te prometes que esos métodos deberían ignorarse en la clase derivada? ¿O te encuentras reemplazando a los métodos de la superclase, haciendo que no funcionen para que nadie los llame inadvertidamente? ¿O dar sugerencias a su herramienta de generación de docs API para omitir el método del documento?

Esas son claras pistas de que la agregación es la mejor opción en ese caso.

6

Veo muchas respuestas "es-a frente a tiene-una; son conceptualmente diferentes" sobre esta y las preguntas relacionadas.

Lo único que he encontrado en mi experiencia es que intentar determinar si una relación es "is-a" o "has-a" está destinada a fallar. Incluso si puede hacer correctamente esa determinación para los objetos ahora, el cambio de requisitos significa que probablemente se equivocará en algún momento en el futuro.

Otra cosa que he encontrado es que es muy muy difícil de convertir de la herencia a la agregación una vez que hay una gran cantidad de código escrito en torno a 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.

Por lo tanto, usted tiene una tormenta perfecta de argumentos en contra de la herencia cada vez que tenga que elegir uno u otro:

  1. Su elección probablemente será la equivocada en algún momento
  2. Cambiar esa elección es difícil una vez que lo hayas hecho
  3. La herencia tiende a ser una opción peor ya que es más restrictiva.

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

+0

Los requisitos en desarrollo significan que su diseño OO será inevitablemente incorrecto. La herencia frente a la agregación es solo la punta de ese iceberg. No se puede diseñar para todos los futuros posibles, así que simplemente vaya con la idea de XP: resuelva los requisitos de hoy y acepte que puede tener que refactorizar. –

+0

Me gusta esta línea de investigación y preguntas. Preocupado por blurrung is-a, has-a, y uses-a though. Empiezo con interfaces (junto con la identificación de las abstracciones implementadas a las que quiero interfaces). El nivel adicional de indirección es invaluable. No sigas tu conclusión de agregación. – orcmid

+0

Eso 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 penalizaciones cuando tienes que resolver problemas mañana. –

3

Quería hacer un comentario sobre la pregunta original, pero muerde 300 caracteres [; <).

Creo que debemos tener cuidado. En primer lugar, 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 apoye el logro del objetivo primario, pero no creo que sea fuera de contexto que la discusión técnica sea la mejor es muy útil. Ayuda a conocer las trampas de los diferentes enfoques junto con sus claros puntos dulces.

Por ejemplo, ¿qué vas a lograr, qué tienes disponible para empezar y cuáles son las limitaciones?

¿Está 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? ¿Se puede separar la estructura de herencia de las interfaces (si existe) 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? ¿Hay varias implementaciones que se pueden usar al mismo tiempo o la variación es 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 una metodología.

Por último, ¿es tan importante bloquear las distinciones en la abstracción y cómo se piensa (como en is-a versus has-a) con las diferentes características de la tecnología OO? Tal vez sea así, si mantiene la estructura conceptual consistente y manejable para usted y los demás. Pero es sabio no ser esclavizado por eso y las contorsiones que podrías terminar haciendo. Tal vez lo mejor sea retroceder un nivel y no ser tan rígido (pero dejar una buena narración para que otros puedan decir qué pasa). [Busco lo que hace que una porción particular de un programa sea explicable, pero algunas veces me decantan por la elegancia cuando hay una ganancia mayor. No siempre es la mejor idea.]

Soy un purista de la interfaz, y me atraen los tipos de problemas y enfoques donde el purismo de interfaz es apropiado, ya sea construyendo un marco Java u organizando algunas implementaciones 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 interfaz, por lo que será interesante ver cómo me las arreglo).

1

El favor ocurre cuando ambos candidatos califican. A y B son opciones y usted favorece a 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 se ve de inmediato. Para ver el beneficio que necesita para esperar la próxima solicitud de cambio inesperado. Entonces, en la mayoría de los casos, aquellos que se adhieren a la generlalización fracasan cuando se los compara con aquellos que adoptaron la composición (excepto un caso obvio que se menciona 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 a cuál favorecer y cuándo. La regla lo ayuda a tomar una decisión también; si no está seguro, seleccione la composición.

Resumen: Composición: El acoplamiento se reduce simplemente por tener algunas cosas más pequeñas que enchufar en algo más grande, y el objeto más grande simplemente llama al objeto más pequeño hacia atrás. Generlización: desde el punto de vista de la API, definir que un método puede ser anulado es un compromiso más fuerte que definir que se pueda llamar a un método. (muy pocas ocasiones cuando gana Generalización). Y nunca olvides que con la composición también estás usando herencia, desde una interfaz en lugar de una clase grande