¿Es un buen concepto usar herencia múltiple o puedo hacer otras cosas en su lugar?
¿Es un buen concepto usar herencia múltiple o puedo hacer otras cosas en su lugar?
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 :
fuente
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:
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.
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.
fuente
GrandParent
enChild
. 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 .
Manera moderna de resolver esto para usar la interfaz (clase abstracta pura) como la interfaz COM y Java.
Sí tu puedes. Voy a robar de GoF .
fuente
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.
fuente
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.
fuente
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
significa que
class B
deriva declass A
. Tenga en cuenta que, dadodecimos que C deriva de B que deriva de A, por lo que también se dice que C deriva de A, por lo tanto
Además, decimos que para cada clase de la
A
queA
deriva trivialmenteA
, nuestro modelo de herencia cumple con la definición de una categoría. En un lenguaje más tradicional, tenemos una categoríaClass
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:
Es un diagrama de aspecto sombrío, pero servirá. Así que
D
hereda de todosA
,B
yC
. Además, y cada vez más cerca de abordar la pregunta de OP,D
también hereda de cualquier superclase deA
. Podemos dibujar un diagramaAhora, los problemas asociados con el Diamante de la Muerte aquí son cuándo
C
yB
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
yC
ser tales que siB
yC
heredar deQ
entoncesA
se puedan reescribir como una subclase deQ
. Esto hace queA
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 ambosB
yC
. Es decir, si tiene cualquier otra clase deR
herencia múltipleB
yC
, entonces,D
es una clase dondeR
se puede reescribir como una subclase deD
.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.
fuente
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! :-)
fuente
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
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.
fuente
Debe usarlo con cuidado, hay algunos casos, como el problema del diamante , en que las cosas pueden complicarse.
(fuente: learncpp.com )
fuente
Printer
ni siquiera debería ser aPoweredDevice
. APrinter
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.Usos y abusos de la herencia.
El artículo hace un gran trabajo al explicar la herencia, y es peligroso.
fuente
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.
fuente
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:
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.
fuente
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.
fuente
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á.
fuente