ventajas tipos poo multiple implementacion herencia desventajas clases c++ oop multiple-inheritance

tipos - ¿Por qué debería evitar la herencia múltiple en C++?



tipos de herencia en java (15)

A riesgo de volverme un poco abstracto, me resulta esclarecedor pensar en la herencia dentro del marco de la teoría de categorías.

Si pensamos en todas nuestras clases y flechas entre ellas que denotan relaciones de herencia, entonces algo como esto

A --> B

significa que la class B deriva de la class A Tenga en cuenta que, dado

A --> B, B --> C

decimos que C deriva de B, que deriva de A, así que también se dice que C deriva de A, así

A --> C

Además, decimos que para cada clase A que trivialmente A deriva de A , nuestro modelo de herencia cumple la definición de categoría. En un lenguaje más tradicional, tenemos una categoría Class con objetos todas las clases y morfismos las relaciones de herencia.

Eso es un poco de configuración, pero con eso echemos un vistazo a nuestro Diamond of Doom:

C --> D ^ ^ | | A --> B

Es un diagrama sombrío, pero servirá. Entonces D hereda de A , B y C Además, y cada vez más cerca de abordar la pregunta de OP, D también hereda de cualquier superclase de A Podemos dibujar un diagrama

C --> D --> R ^ ^ | | A --> B ^ | Q

Ahora, los problemas asociados con el Diamante de la Muerte aquí son cuando C y B comparten algunos nombres de propiedad / método y las cosas se vuelven ambiguas; sin embargo, si movemos cualquier comportamiento compartido a A entonces la ambigüedad desaparece.

Dicho en términos categóricos, queremos que A , B y C sean tales que si B y C heredan de Q entonces A puede reescribirse como subclase de Q Esto hace que A algo se lo llame " pushout .

También hay una construcción simétrica en D llamada pullback . Esta es esencialmente la clase útil más general que puede construir que hereda tanto de B como de C Es decir, si tiene cualquier otra clase R heredando de B y C , entonces D es una clase donde R puede reescribirse como subclase de D

Asegurarse de que sus consejos sobre el diamante sean retrocesos e impulsos nos da una buena manera de manejar genéricamente los problemas de mantenimiento o enfrentamiento de nombres que de otro modo podrían surgir.

Nota La answer inspiró esto ya que sus admoniciones están implícitas en el modelo anterior dado que trabajamos en la categoría de categoría completa de todas las clases posibles.

Quería generalizar su argumento a algo que muestra cuán complicadas son las relaciones de herencia múltiple que pueden ser poderosas y no problemáticas.

TL; DR Piense en las relaciones de herencia en su programa como formando una categoría. Entonces puedes evitar los problemas del Diamante de la Perdición haciendo pushouts de clases heredadas de forma múltiple y simétricamente, creando una clase parental común que es un retroceso.

¿Es un buen concepto usar herencia múltiple o puedo hacer otras cosas en su lugar?


Cada lenguaje de programación tiene un tratamiento ligeramente diferente de la programación orientada a objetos con pros y contras. La versión de C ++ pone el énfasis directamente en el rendimiento y tiene la desventaja acompañante de que es inquietantemente fácil escribir código no válido, y esto es cierto para la herencia múltiple. Como consecuencia, hay una tendencia a alejar a los programadores de esta función.

Otras personas han abordado la cuestión de para qué herencia múltiple no es bueno. Pero hemos visto bastantes comentarios que más o menos implican que la razón para evitarlo es porque no es seguro. Bueno, sí y no.

Como suele ocurrir en C ++, si sigue una guía básica puede usarla de forma segura sin tener que "mirar por encima del hombro" constantemente. La idea clave es distinguir un tipo especial de definición de clase llamada "mezcla"; class es una mezcla si todas sus funciones miembro son virtuales (o virtuales puras). Luego puede heredar de una única clase principal y tantas "mezclas" como desee, pero debe heredar mixins con la palabra clave "virtual". p.ej

class CounterMixin { int count; public: CounterMixin() : count( 0 ) {} virtual ~CounterMixin() {} virtual void increment() { count += 1; } virtual int getCount() { return count; } }; class Foo : public Bar, virtual public CounterMixin { ..... };

Mi sugerencia es que si tiene la intención de utilizar una clase como una clase mixta, también adopte una convención de nomenclatura para que sea fácil para cualquiera que lea el código ver lo que está sucediendo y para verificar que está siguiendo las reglas de la guía básica. . Y encontrará que funciona mucho mejor si sus mix-ins también tienen constructores predeterminados, solo por la forma en que funcionan las clases base virtuales. Y recuerda hacer todos los destructores virtuales también.

Tenga en cuenta que mi uso de la palabra "mezclar" aquí no es lo mismo que la clase de plantilla parametrizada (consulte este enlace para obtener una buena explicación), pero creo que es un uso justo de la terminología.

Ahora no quiero dar la impresión de que esta es la única forma de usar herencia múltiple de forma segura. Es solo una manera que es bastante fácil de verificar.


De una entrevista con Bjarne Stroustrup :

La gente dice correctamente que no necesita herencia múltiple, porque cualquier cosa que pueda hacer con la herencia múltiple también lo puede hacer con la herencia individual. Solo usa el truco de delegación que mencioné. Además, no necesita ninguna herencia en absoluto, porque cualquier cosa que haga con la herencia individual también puede prescindir de la herencia reenviando a través de una clase. En realidad, tampoco necesita ninguna clase, porque puede hacerlo todo con punteros y estructuras de datos. Pero, ¿por qué querrías hacer eso? ¿Cuándo es conveniente usar las instalaciones de idioma? ¿Cuándo preferirías una solución alternativa? He visto casos en los que la herencia múltiple es útil, e incluso he visto casos en los que la herencia múltiple bastante complicada es útil. En general, prefiero utilizar las instalaciones que ofrece el idioma para hacer soluciones



El problema clave con MI de los objetos concretos es que rara vez tiene un objeto que legítimamente debería "Ser un A Y SER un B", por lo que rara vez es la solución correcta por razones lógicas. Mucho más a menudo, tiene un objeto C que obedece "C puede actuar como A o B", lo que puede lograr a través de la herencia y composición de la interfaz. Pero no se equivoque: la herencia de múltiples interfaces sigue siendo MI, solo un subconjunto de ella.

Para C ++ en particular, la debilidad clave de la función no es la EXISTENCIA real de Herencia Múltiple, sino algunas construcciones que permite que casi siempre están mal formadas. Por ejemplo, heredar copias múltiples del mismo objeto como:

class B : public A, public A {};

está malformado POR DEFINICIÓN. Traducido al inglés, esto es "B es una A y una A". Entonces, incluso en lenguaje humano hay una ambigüedad severa. ¿Querías decir "B tiene 2 como" o simplemente "B es una A" ?. Permitir ese código patológico y, lo que es peor, convertirlo en un ejemplo de uso, C ++ no se mostró favorable a la hora de justificar el mantenimiento de la función en los idiomas sucesores.


La herencia múltiple (abreviada como MI) huele , lo que significa que, por lo general , se hizo por malas razones, y se repelirá ante el mantenedor.

Resumen

  1. Considere la composición de las características, en lugar de la herencia
  2. Tenga cuidado con el Diamante del Pavor
  3. Considere la herencia de múltiples interfaces en lugar de objetos
  4. A veces, la Herencia Múltiple es lo correcto. Si lo es, entonces úselo.
  5. Prepárese para defender su arquitectura de herencia múltiple en revisiones de código

1. Tal vez la composición?

Esto es cierto para la herencia, y por lo tanto, es aún más cierto para la herencia múltiple.

¿Su objeto realmente necesita heredar de otro? Un Car no necesita heredar de un Engine para trabajar, ni de una Wheel . Un Car tiene un Engine y cuatro Wheel .

Si usa herencia múltiple para resolver estos problemas en lugar de composición, entonces ha hecho algo mal.

2. El diamante del pavor

Normalmente, tienes una clase A , luego B y C ambos heredan de A Y (no me preguntes por qué) alguien decide que D debe heredar ambos de B y C

Me he encontrado con este tipo de problema dos veces en 8 ochos años, y es divertido verlo debido a:

  1. ¿Cuánto cometió un error desde el principio? (En ambos casos, D no debería haber heredado tanto de B como de C ), porque era una mala arquitectura (de hecho, C no debería haber existido en absoluto ...)
  2. Cuánto estaban pagando los mantenedores por eso, porque en C ++, la clase padre A estaba presente dos veces en su clase de nieto D , y por lo tanto, actualizar un campo padre A::field significaba actualizarlo dos veces (a través de B::field y C::field ), o tener algo en silencio y colapsar, más tarde (nuevo puntero en el B::field y eliminar el C::field ...)

Usar la palabra clave virtual en C ++ para calificar la herencia evita el doble diseño descrito anteriormente si esto no es lo que quiere, pero de todos modos, en mi experiencia, probablemente esté haciendo algo mal ...

En la jerarquía de objetos, debe intentar mantener la jerarquía como un árbol (un nodo tiene UN padre), no como un gráfico.

Más sobre el diamante (editar 2017-05-03)

El verdadero problema con el Diamante de terror en C ++ ( suponiendo que el diseño es sólido, ¡se ha revisado tu código! ), Es que debes tomar una decisión :

  • ¿Es deseable que la clase A exista dos veces en su diseño y qué significa? Si es así, entonces, por supuesto, herede dos veces.
  • si debería existir solo una vez, entonces hereda de ella virtualmente.

Esta elección es inherente al problema, y ​​en C ++, a diferencia de otros idiomas, puede hacerlo sin dogma forzando su diseño a nivel de lenguaje.

Pero al igual que todos los poderes, con ese poder viene la responsabilidad: haga revisar su diseño.

3. Interfaces

La herencia múltiple de cero o una clase concreta, y cero o más interfaces por lo general está bien, porque no encontrará el Diamante de pavor descrito anteriormente. De hecho, así es como se hacen las cosas en Java.

Usualmente, lo que quiere decir cuando C hereda de A y B es que los usuarios pueden usar C como si fuera A , y / o como si fuera B

En C ++, una interfaz es una clase abstracta que tiene:

  1. todo su método declarado puro virtual (con el sufijo = 0) (eliminado el 2017-05-03)
  2. sin variables miembro

La herencia múltiple de cero a un objeto real, y cero o más interfaces no se considera "maloliente" (al menos, no tanto).

Más sobre la interfaz abstracta de C ++ (edición 2017-05-03)

En primer lugar, el patrón NVI se puede utilizar para producir una interfaz, porque el criterio real es no tener ningún estado (es decir, no hay variables miembro, excepto this ). El punto de su interfaz abstracta es publicar un contrato ("puede llamarme de esta manera, y de esta manera"), nada más, nada menos. La limitación de tener solo un método virtual abstracto debe ser una elección de diseño, no una obligación.

En segundo lugar, en C ++, tiene sentido heredar virtualmente de interfaces abstractas (incluso con el costo / indirección adicional). Si no lo hace, y la herencia de la interfaz aparece varias veces en su jerarquía, entonces tendrá ambigüedades.

En tercer lugar, la orientación a objetos es excelente, pero no es The Only Truth Out There TM en C ++. Use las herramientas adecuadas y siempre recuerde que tiene otros paradigmas en C ++ que ofrecen diferentes tipos de soluciones.

4. ¿Realmente necesita Herencia Múltiple?

A veces sí.

Por lo general, su clase C hereda de A y B , y A y B son dos objetos no relacionados (es decir, no en la misma jerarquía, nada en común, conceptos diferentes, etc.).

Por ejemplo, podría tener un sistema de Nodes con coordenadas X, Y, Z, capaz de hacer muchos cálculos geométricos (quizás un punto, parte de objetos geométricos) y cada Nodo es un Agente Automatizado, capaz de comunicarse con otros agentes .

Tal vez ya tiene acceso a dos bibliotecas, cada una con su propio espacio de nombres (otra razón para usar espacios de nombres ... Pero usa espacios de nombres, ¿no?), Uno es geo y el otro es ai

Así que usted tiene su propio own::Node deriva ambos de ai::Agent y geo::Point .

Este es el momento en el que deberías preguntarte si no deberías usar composición en su lugar. Si own::Node realmente es tanto un ai::Agent como un geo::Point , entonces la composición no funcionará.

Entonces necesitarás herencia múltiple, teniendo tu own::Node comunicarte con otros agentes de acuerdo a su posición en un espacio tridimensional.

(Notarás que ai::Agent y geo::Point están completa, totalmente, completamente desvinculados ... Esto reduce drásticamente el peligro de herencia múltiple)

Otros casos (editar 2017-05-03)

Hay otros casos:

  • usando la herencia (con suerte privada) como detalle de implementación
  • algunos modismos de C ++, como las políticas, pueden usar herencia múltiple (cuando cada parte necesita comunicarse con los demás a través de this )
  • la herencia virtual de std :: exception ( ¿Es necesaria la herencia virtual para las excepciones? )
  • etc.

Algunas veces puedes usar composición, y algunas veces MI es mejor. El punto es: usted tiene una opción. Hágalo responsablemente (y haga revisar su código).

5. Entonces, ¿debería hacer Herencia Múltiple?

La mayoría de las veces, en mi experiencia, no. MI no es la herramienta adecuada, incluso si parece funcionar, ya que puede ser utilizada por las funciones de perezoso a acumularse sin darse cuenta de las consecuencias (como hacer que un Car tanto un Engine como una Wheel ).

Pero a veces, sí. Y en ese momento, nada funcionará mejor que MI.

Pero como MI es maloliente, prepárate para defender tu arquitectura en revisiones de código (y defenderlo es algo bueno, porque si no puedes defenderlo, entonces no deberías hacerlo).


La herencia pública es una relación IS-A, y algunas veces una clase será un tipo de varias clases diferentes, y algunas veces es importante reflejar esto.

"Mixins" también son a veces útiles. Por lo general, son clases pequeñas, generalmente no heredan de nada, lo que proporciona una funcionalidad útil.

Siempre que la jerarquía de herencia sea bastante superficial (como debería ser casi siempre) y bien administrada, es poco probable que obtenga la temida herencia de diamantes. El diamante no es un problema con todos los lenguajes que usan herencia múltiple, pero el tratamiento de C ++ con frecuencia es incómodo y algunas veces desconcertante.

Si bien me he encontrado con casos en los que la herencia múltiple es muy útil, en realidad son bastante raros. Esto es probable porque prefiero usar otros métodos de diseño cuando realmente no necesito herencia múltiple. Prefiero evitar construcciones de lenguaje confusas, y es fácil construir casos de herencia en los que hay que leer el manual muy bien para descubrir qué está pasando.


Más allá del patrón de diamantes, la herencia múltiple tiende a dificultar la comprensión del modelo de objetos, lo que a su vez aumenta los costos de mantenimiento.

La composición es intrínsecamente fácil de comprender, comprender y explicar. Puede ser tedioso escribir código para, pero un buen IDE (han pasado algunos años desde que trabajé con Visual Studio, pero sin duda los IDE de Java tienen excelentes herramientas de automatización de acceso directo a la composición) debería superar ese obstáculo.

Además, en términos de mantenimiento, el "problema del diamante" también aparece en instancias de herencia no literales. Por ejemplo, si tienes A y B y tu clase C los extiende a ambos, y A tiene un método ''makeJuice'' que produce jugo de naranja y lo extiendes para hacer jugo de naranja con un toque de lima: ¿qué ocurre cuando el diseñador hace '' B ''agrega un método'' makeJuice ''que genera y corriente eléctrica? ''A'' y ''B'' pueden ser compatibles "padres" en este momento , ¡pero eso no significa que siempre lo serán!

En general, la máxima de la tendencia a evitar la herencia, y especialmente la herencia múltiple, es sólida. Como todas las máximas, hay excepciones, pero debes asegurarte de que haya un letrero de neón verde intermitente que apunte a cualquier excepción que codifiques (y entrenar tu cerebro para que cada vez que veas esos árboles de herencia dibujes en tu propio neón verde intermitente) signo), y que revise para asegurarse de que todo tenga sentido de vez en cuando.


No debe "evitar" la herencia múltiple, pero debe tener en cuenta los problemas que pueden surgir, como el "problema de los diamantes" ( http://en.wikipedia.org/wiki/Diamond_problem ) y tratar el poder que se le da con cuidado , como deberías con todos los poderes.


No hay razón para evitarlo y puede ser muy útil en situaciones. Sin embargo, debes ser consciente de los posibles problemas.

El más grande es el diamante de la muerte:

class GrandParent; class Parent1 : public GrandParent; class Parent2 : public GrandParent; class Child : public Parent1, public Parent2;

Ahora tiene dos "copias" de GrandParent dentro de Child.

Sin embargo, C ++ ha pensado en esto y le permite hacer herencia virtual para evitar los problemas.

class GrandParent; class Parent1 : public virtual GrandParent; class Parent2 : public virtual GrandParent; class Child : public Parent1, public Parent2;

Siempre revise su diseño, asegúrese de no estar usando la herencia para ahorrar en la reutilización de datos. Si puedes representar lo mismo con la composición (y normalmente puedes) este es un enfoque mucho mejor.


Puede usar la composición en preferencia a la herencia.

El sentimiento general es que la composición es mejor y se discute muy bien.


Usamos Eiffel. Tenemos un excelente MI. Sin preocupaciones. Sin problemas. Gestionado fácilmente Hay veces para NO usar MI. Sin embargo, es útil más de lo que las personas saben porque son: A) en un lenguaje peligroso que no lo maneja bien -O- B) satisfecho con la forma en que han trabajado con MI durante años y años -O- C) otras razones ( demasiado numeroso para enumerarlo estoy bastante seguro - ver las respuestas arriba).

Para nosotros, usar Eiffel, MI es tan natural como cualquier otra cosa y otra herramienta excelente en la caja de herramientas. Francamente, no nos preocupa que nadie más esté usando Eiffel. Sin preocupaciones. Estamos contentos con lo que tenemos y lo invitamos a echar un vistazo.

Mientras mira: tome nota especial de Void-safety y la erradicación de la desreferenciación de punteros nulos. ¡Mientras todos bailamos alrededor de MI, tus indicadores se están perdiendo! :-)


Ver w: Herencia Múltiple .

La herencia múltiple ha recibido críticas y, como tal, no se implementa en muchos idiomas. Las críticas incluyen:

  • Mayor complejidad
  • La ambigüedad semántica a menudo se resume como el problema del diamante .
  • No ser capaz de heredar explícitamente varias veces desde una sola clase
  • Orden de herencia cambiando la semántica de clase.

La herencia múltiple en lenguajes con constructores de estilo C ++ / Java agrava el problema de herencia de los constructores y el encadenamiento de constructores, creando así problemas de mantenimiento y extensibilidad en estos lenguajes. Los objetos en relaciones de herencia con métodos de construcción muy variables son difíciles de implementar bajo el paradigma de encadenamiento de constructores.

Manera moderna de resolver esto para utilizar la interfaz (clase abstracta pura) como la interfaz COM y Java.

Puedo hacer otras cosas en lugar de esto?

Sí tu puedes. Voy a robar de GoF .

  • Programa a una interfaz, no a una implementación
  • Prefiere la composición sobre la herencia

toma 4/8 bytes por clase involucrada. (Uno este puntero por clase).

Puede que esto nunca sea una preocupación, pero si algún día tienes una estructura de microdatos que es instanciada miles de millones de tiempo, será.