Respuestas:
La herencia múltiple (abreviada como MI) huele , lo que significa que generalmente mal , por lo , se hizo por malas razones, y volverá a golpear al mantenedor.
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? A Car
no necesita heredar de a Engine
para trabajar, ni de a Wheel
. A Car
tiene un Engine
y cuatro Wheel
.
Si usa la herencia múltiple para resolver estos problemas en lugar de la composición, entonces ha hecho algo mal.
Por lo general, tiene una clase A
, B
y C
ambas heredan de A
. Y (no me pregunten por qué) a alguien, entonces decide que D
debe heredar tanto desde B
yC
.
Me he encontrado con este tipo de problema dos veces en 8 años, y es divertido verlo por:
D
no debería haber heredado de ambos B
y C
), porque esta era una mala arquitectura (de hecho, C
no debería haber existido en absoluto ...)A
estaba presente dos veces en su clase de nietos D
, y por lo tanto, actualizar un campo principal A::field
significaba actualizarlo dos veces (a través de B::field
y C::field
), o hacer que algo salga silenciosamente mal y se bloquee, más tarde (Nuevo puntero B::field
y eliminar C::field
...)El uso de la palabra clave virtual en C ++ para calificar la herencia evita el diseño doble descrito anteriormente si esto no es lo que desea, 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.
El verdadero problema con Diamond of Dread en C ++ ( suponiendo que el diseño sea sólido, ¡revise su código! ), Es que debe elegir :
A
exista dos veces en su diseño, y qué significa? Si es así, entonces herede de él dos veces.Esta elección es inherente al problema, y en C ++, a diferencia de otros lenguajes, puede hacerlo sin dogmas que obliguen a su diseño a nivel de lenguaje.
Pero como todos los poderes, con ese poder viene la responsabilidad: hacer revisar su diseño.
La herencia múltiple de cero o una clase concreta, y cero o más interfaces generalmente está bien, porque no encontrará el Diamante del terror descrito anteriormente. De hecho, así es como se hacen las cosas en Java.
Por lo general, lo que quiere decir cuando C hereda de A
y B
es que los usuarios pueden usar C
como si fuera un A
y / o como si fuera un B
.
En C ++, una interfaz es una clase abstracta que tiene:
La herencia múltiple de cero a un objeto real, y cero o más interfaces no se considera "maloliente" (al menos, no tanto).
Primero, el patrón NVI se puede usar para producir una interfaz, porque el criterio real es no tener estado (es decir, no hay variables miembro, excepto this
). El objetivo de su interfaz abstracta es publicar un contrato ("puede llamarme de esta manera y de esta manera"), nada más y 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 adicional / indirección). 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 la única verdad en TM en C ++. Use las herramientas adecuadas y recuerde siempre que tiene otros paradigmas en C ++ que ofrecen diferentes tipos de soluciones.
A veces sí.
Por lo general, su C
clase 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, diferentes conceptos, etc.).
Por ejemplo, podría tener un sistema 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.
Quizás ya tenga acceso a dos bibliotecas, cada una con su propio espacio de nombres (otra razón para usar espacios de nombres ... Pero usted usa espacios de nombres, ¿no?), Un ser geo
y el otro serai
Entonces tienes tu propio own::Node
derivado de ai::Agent
y geo::Point
.
Este es el momento en que debes preguntarte si no deberías usar composición en su lugar. Si own::Node
realmente es tanto a ai::Agent
como a geo::Point
, entonces la composición no servirá.
Entonces necesitará herencia múltiple, own::Node
comunicándose con otros agentes de acuerdo con su posición en un espacio 3D.
(Notará eso ai::Agent
y estará geo::Point
completamente, totalmente, SIN RELACIÓN ... Esto reduce drásticamente el peligro de herencia múltiple)
Hay otros casos:
this
)Algunas veces puedes usar composición, y otras veces el MI es mejor. El punto es: tienes una opción. Hágalo responsablemente (y revise su código).
La mayoría de las veces, en mi experiencia, no. MI no es la herramienta correcta, incluso si parece funcionar, porque puede ser utilizada por los perezosos para agrupar características sin darse cuenta de las consecuencias (como hacer un Car
an Engine
y un a Wheel
).
Pero a veces sí. Y en ese momento, nada funcionará mejor que MI.
Pero debido a que MI es maloliente, prepárate para defender tu arquitectura en las revisiones de código (y defenderlo es algo bueno, porque si no eres capaz de defenderlo, entonces no debes hacerlo).
De una entrevista con Bjarne Stroustrup :
La gente dice correctamente que no necesita herencia múltiple, porque todo lo que puede hacer con herencia múltiple también puede hacerlo con herencia única. Simplemente usa el truco de delegación que mencioné. Además, no necesita ninguna herencia en absoluto, porque cualquier cosa que haga con herencia única también puede prescindir de la herencia mediante el reenvío 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 utilizar las instalaciones de idiomas? ¿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 usar las facilidades que ofrece el idioma para hacer soluciones
const
: he tenido que escribir soluciones alternativas torpes (generalmente usando interfaces y composición) cuando una clase realmente necesitaba tener variantes mutables e inmutables. Sin embargo, nunca he una vez perdida la herencia múltiple, y nunca he sentido que tenía que escribir una solución debido a la falta de esta característica. Esa es la diferencia. En todos los casos que he visto, no usar MI es la mejor opción de diseño, no una solución alternativa.
No hay razón para evitarlo y puede ser muy útil en situaciones. Sin embargo, debe 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 una herencia virtual para solucionar 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 puede representar lo mismo con la composición (y normalmente puede hacerlo), este es un enfoque mucho mejor.
GrandParent
en Child
. Existe un temor al infarto de miocardio, porque las personas simplemente piensan que podrían no entender las reglas del idioma. Pero cualquiera que no pueda obtener estas reglas simples tampoco puede escribir un programa no trivial.
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 poder heredar explícitamente varias veces de una sola clase
- Orden de herencia que cambia la semántica de clases.
La herencia múltiple en lenguajes con constructores de estilo C ++ / Java exacerba 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 usar 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 .
La herencia pública es una relación IS-A, y a veces una clase será un tipo de varias clases diferentes, y a veces es importante reflejar esto.
Las "mixinas" también son a veces útiles. En general, son clases pequeñas, que generalmente no se heredan de nada, lo que proporciona una funcionalidad útil.
Siempre que la jerarquía de herencia sea bastante superficial (como casi siempre debería ser) y esté 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 ++ es con frecuencia incómodo y a 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 confusos construcciones de lenguaje, y es fácil construir casos de herencia en los que tienes que leer el manual realmente bien para descubrir qué está pasando.
No debe "evitar" la herencia múltiple, pero debe tener en cuenta los problemas que pueden surgir, como el 'problema del diamante' ( http://en.wikipedia.org/wiki/Diamond_problem ) y tratar con cuidado el poder que se le otorga. , como deberías con todos los poderes.
A riesgo de ser un poco abstracto, me resulta iluminador 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 class B
deriva de class A
. Tenga en cuenta que, dado
A --> B, B --> C
decimos que C deriva de B que deriva de A, por lo que también se dice que C deriva de A, por lo tanto
A --> C
Además, decimos que para cada clase de la A
que A
deriva trivialmente A
, nuestro modelo de herencia cumple con la definición de una categoría. En un lenguaje más tradicional, tenemos una categoría Class
con objetos de todas las clases y morfismos de 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 de aspecto sombrío, pero servirá. Así que D
hereda de todos 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 cuándo C
y B
compartir algunos nombres de propiedades / métodos y las cosas se vuelven ambiguas; sin embargo, si trasladamos cualquier comportamiento compartido, A
la ambigüedad desaparece.
En términos categóricos, queremos A
, B
y C
ser tales que si B
y C
heredar de Q
entonces A
se puedan reescribir como una subclase de Q
. Esto hace que A
algo se llame pushout .
También hay una construcción simétrica D
llamada retroceso . Esta es esencialmente la clase útil más general que puede construir que hereda de ambos B
y C
. Es decir, si tiene cualquier otra clase de R
herencia múltiple B
y C
, entonces, D
es una clase donde R
se puede reescribir como una subclase de D
.
Asegurarse de que sus puntas del diamante sean retrocesos y flexiones nos brinda una buena manera de manejar genéricamente los problemas de nombres o de mantenimiento que podrían surgir de otra manera.
Tenga en cuenta que la respuesta de Paercebal inspiró esto, ya que sus advertencias están implícitas en el modelo anterior dado que trabajamos en la categoría de clase 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. Luego, puede evitar los problemas de Diamond of Doom al hacer que las clases heredadas se eliminen y simétricamente, creando una clase principal común que es un retroceso.
Usamos Eiffel. Tenemos excelente MI. Sin preocupaciones. Sin problemas. Fácilmente gestionado. Hay momentos para NO usar MI. Sin embargo, es más útil de lo que las personas se dan cuenta porque están: A) en un lenguaje peligroso que no lo maneja bien -O BIEN- B) satisfecho con la forma en que han trabajado con MI durante años y años -O BIEN- C) otras razones ( demasiado numeroso para enumerarlo, estoy bastante seguro, vea las respuestas anteriores).
Para nosotros, usar Eiffel, MI es tan natural como cualquier otra cosa y otra buena herramienta 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 te invitamos a echar un vistazo.
Mientras busca: tome nota especial de la seguridad de vacío y la erradicación de la anulación de referencia de puntero nulo. ¡Mientras todos bailamos alrededor de MI, sus punteros se están perdiendo! :-)
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 el inconveniente de que es inquietantemente fácil escribir código invá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 pregunta de para qué no es buena la herencia múltiple. Pero hemos visto bastantes comentarios que implican más o menos que la razón para evitarlo es porque no es seguro. Pues sí y no.
Como suele suceder en C ++, si sigue una directriz básica, puede usarla de manera 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"; La clase es una combinación si todas sus funciones miembro son virtuales (o virtuales). Entonces se le permite heredar de una sola clase principal y tantos "mix-ins" 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 usar una clase como una clase mixta, también adopta una convención de nomenclatura para facilitar que cualquiera que lea el código vea lo que está sucediendo y verifique que está jugando según las reglas de la guía básica. . Y encontrará que funciona mucho mejor si sus mezclas 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 (vea este enlace para 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 la herencia múltiple de forma segura. Es solo una forma que es bastante fácil de verificar.
Debe usarlo con cuidado, hay algunos casos, como el problema del diamante , en que las cosas pueden complicarse.
(fuente: learncpp.com )
Printer
ni siquiera debería ser a PoweredDevice
. A Printer
es para imprimir, no para administrar la energía. La implementación de una impresora en particular podría tener que administrar algo de energía, pero estos comandos de administración de energía no deberían exponerse directamente a los usuarios de la impresora. No puedo imaginar el uso de esta jerarquía en el mundo real.
El artículo hace un gran trabajo al explicar la herencia, y es peligroso.
Más allá del patrón de diamante, 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 entender, comprender y explicar. Puede ser tedioso escribir código, pero un buen IDE (han pasado algunos años desde que trabajé con Visual Studio, pero ciertamente los IDE de Java tienen excelentes herramientas de automatización de atajos de composición) deberían superar ese obstáculo.
Además, en términos de mantenimiento, el "problema del diamante" surge también en casos de herencia no literal. Por ejemplo, si tiene A y B y su clase C los extiende a ambos, y A tiene un método 'makeJuice' que hace jugo de naranja y usted lo extiende para hacer jugo de naranja con un toque de lima: ¿qué sucede cuando el diseñador de ' B 'agrega un método' makeJuice 'que genera y corriente eléctrica? 'A' y 'B' pueden ser "padres" compatibles en este momento , ¡pero eso no significa que siempre lo serán!
En general, la máxima de tender a evitar la herencia, y especialmente la herencia múltiple, es sólida. Como todas las máximas, hay excepciones, pero debe asegurarse de que haya un letrero de neón verde intermitente que señale cualquier excepción que codifique (y entrene su cerebro para que cada vez que vea tales árboles de herencia dibuje en su propio neón verde intermitente signo), y que verifique para asegurarse de que todo tenga sentido de vez en cuando.
what happens when the designer for 'B' adds a 'makeJuice' method which generates and electrical current?
Uhhh, obtienes un error de compilación, por supuesto (si el uso es ambiguo).
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. Con mucha más frecuencia, 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 varias interfaces sigue siendo MI, solo un subconjunto.
Para C ++ en particular, la debilidad clave de la característica no es la EXISTENCIA real de herencia múltiple, pero algunas construcciones que permite casi siempre tienen malformaciones. Por ejemplo, heredar varias copias 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 el lenguaje humano hay una gran ambigüedad. ¿Quiso decir "B tiene 2 As" o simplemente "B es una A"? Permitir dicho código patológico, y lo que es peor, convertirlo en un ejemplo de uso, C ++ no favoreció a la hora de defender la función en lenguajes sucesores.
Puede usar la composición con preferencia a la herencia.
El sentimiento general es que la composición es mejor, y está muy bien discutida.
The general feeling is that composition is better, and it's very well discussed.
Eso no significa que la composición sea mejor.
toma 4/8 bytes por clase involucrada. (Uno este puntero por clase).
Esto podría nunca ser una preocupación, pero si algún día tiene una estructura de microdatos que se ejecuta miles de millones de veces, lo será.