Siempre me ha gustado la idea de tener herencia múltiple compatible en un idioma. La mayoría de las veces, aunque se olvida intencionalmente, y el supuesto "reemplazo" son las interfaces. Las interfaces simplemente no cubren el mismo terreno que la herencia múltiple, y esta restricción ocasionalmente puede conducir a más código repetitivo.
La única razón básica que he escuchado para esto es el problema del diamante con las clases base. Simplemente no puedo aceptar eso. Para mí, parece mucho, "Bueno, es posible arruinarlo, así que automáticamente es una mala idea". Sin embargo, puedes arruinar cualquier cosa en un lenguaje de programación, y quiero decir cualquier cosa. Simplemente no puedo tomar esto en serio, al menos no sin una explicación más exhaustiva.
Solo estar al tanto de este problema es el 90% de la batalla. Además, creo que escuché algo hace años sobre una solución de propósito general que involucra un algoritmo de "sobre" o algo así (¿esto suena una campana, alguien?).
Con respecto al problema del diamante, el único problema potencialmente genuino en el que puedo pensar es si está tratando de usar una biblioteca de terceros y no puede ver que dos clases aparentemente no relacionadas en esa biblioteca tienen una clase base común, pero además de documentación, una característica de lenguaje simple podría, digamos, requerir que declare específicamente su intención de crear un diamante antes de que realmente compile uno para usted. Con tal característica, cualquier creación de un diamante es intencional, imprudente o porque uno no es consciente de esta trampa.
Dicho todo esto ... ¿Hay alguna razón real por la que la mayoría de las personas odie la herencia múltiple, o es solo un montón de histeria que causa más daño que bien? ¿Hay algo que no estoy viendo aquí? Gracias.
Ejemplo
El automóvil extiende el vehículo con ruedas, KIASpectra extiende el automóvil y electrónico, KIASpectra contiene radio. ¿Por qué KIASpectra no contiene Electronic?
Porque es un electrónico. La herencia versus la composición siempre debe ser una relación es-una relación versus una relación tiene-tiene.
Porque es un electrónico. Hay cables, tableros de circuitos, interruptores, etc. arriba y abajo de esa cosa.
Porque es un electrónico. Si su batería se agota en el invierno, está en tantos problemas como si todas sus ruedas desaparecieran repentinamente.
¿Por qué no usar interfaces? Tome el n. ° 3, por ejemplo. No quiero escribir esto una y otra vez, y realmente tampoco quiero crear una extraña clase de ayuda proxy para hacer esto:
private void runOrDont()
{
if (this.battery)
{
if (this.battery.working && this.switchedOn)
{
this.run();
return;
}
}
this.dontRun();
}
(No estamos analizando si esa implementación es buena o mala). Puede imaginar cómo puede haber varias de estas funciones asociadas con Electronic que no están relacionadas con nada en WheeledVehicle, y viceversa.
No estaba seguro de establecer ese ejemplo o no, ya que hay espacio para la interpretación allí. También se podría pensar en términos de Plano extendiendo Vehículo y FlyingObject y Bird extendiendo Animal y FlyingObject, o en términos de un ejemplo mucho más puro.
Traits
: actúan como interfaces con implementación opcional, pero tienen algunas restricciones que ayudan a evitar que surjan problemas como el problema del diamante.KiaSpectra
no es unaElectronic
; que tiene Electrónica, y puede ser unaElectronicCar
(lo que extenderíaCar
...)Respuestas:
En muchos casos, las personas usan la herencia para proporcionar un rasgo a una clase. Por ejemplo, piensa en un Pegaso. Con la herencia múltiple, puede sentirse tentado a decir que el Pegaso extiende Caballo y Pájaro porque ha clasificado al Pájaro como un animal con alas.
Sin embargo, las aves tienen otros rasgos que Pegasi no tiene. Por ejemplo, las aves ponen huevos, los pegasos nacen vivos. Si la herencia es su único medio de pasar rasgos compartidos, entonces no hay forma de excluir el rasgo de desove del Pegaso.
Algunos idiomas han optado por hacer de los rasgos una construcción explícita dentro del lenguaje. Otros te guían suavemente en esa dirección eliminando MI del idioma. De cualquier manera, no puedo pensar en un solo caso en el que pensara "Hombre, realmente necesito MI para hacer esto correctamente".
También discutamos qué es la herencia REALMENTE. Cuando hereda de una clase, depende de esa clase, pero también debe respaldar los contratos que la clase admite, tanto implícitos como explícitos.
Tome el ejemplo clásico de un cuadrado que hereda de un rectángulo. El rectángulo expone una propiedad de longitud y ancho y también un método getPerimeter y getArea. El cuadrado anularía la longitud y el ancho, de modo que cuando uno esté configurado, el otro esté configurado para coincidir con getPerimeter y getArea funcionaría de la misma manera (2 * largo + 2 * ancho para el perímetro y largo * ancho para el área).
Hay un solo caso de prueba que se rompe si sustituye esta implementación de un cuadrado por un rectángulo.
Es lo suficientemente difícil de hacer las cosas bien con una sola cadena de herencia. Se pone aún peor cuando agrega otro a la mezcla.
Las trampas que mencioné con el Pegasus en MI y las relaciones Rectángulo / Cuadrado son el resultado de un diseño inexperto para las clases. Básicamente, evitar la herencia múltiple es una forma de ayudar a los desarrolladores principiantes a evitar dispararse en el pie. Como todos los principios de diseño, tener disciplina y entrenamiento basados en ellos te permite descubrir a tiempo cuándo está bien romper con ellos. Vea el Modelo Dreyfus de Adquisición de Habilidades , en el nivel Experto, su conocimiento intrínseco trasciende la dependencia de las máximas / principios. Puede "sentir" cuando una regla no se aplica.
Y estoy de acuerdo en que de alguna manera hice trampa con un ejemplo del "mundo real" de por qué MI está mal visto.
Veamos un marco de UI. Específicamente, echemos un vistazo a algunos widgets que al principio pueden parecer simplemente una combinación de otros dos. Como un ComboBox. Un ComboBox es un TextBox que tiene un DropDownList compatible. Es decir, puedo escribir un valor, o puedo seleccionar de una lista de valores previamente ordenada. Un enfoque ingenuo sería heredar el ComboBox de TextBox y DropDownList.
Pero su Textbox deriva su valor de lo que el usuario ha escrito. Mientras que el DDL obtiene su valor de lo que selecciona el usuario. ¿Quién toma precedente? El DDL podría haber sido diseñado para verificar y rechazar cualquier entrada que no estuviera en su lista original de valores. ¿Anulamos esa lógica? Eso significa que tenemos que exponer la lógica interna para que los herederos anulen. O peor, agregue lógica a la clase base que solo está allí para admitir una subclase (violando el Principio de Inversión de Dependencia ).
Evitar MI te ayuda a evitar este escollo por completo. Y podría llevarlo a extraer rasgos comunes y reutilizables de sus widgets de IU para que puedan aplicarse según sea necesario. Un excelente ejemplo de esto es la propiedad adjunta de WPF que permite que un elemento de marco en WPF proporcione una propiedad que otro elemento de marco puede usar sin heredar del elemento de marco principal.
Por ejemplo, una cuadrícula es un panel de diseño en WPF y tiene propiedades adjuntas de columna y fila que especifican dónde debe colocarse un elemento secundario en la disposición de la cuadrícula. Sin las propiedades adjuntas, si deseo organizar un botón dentro de una cuadrícula, el botón tendría que derivarse de la cuadrícula para poder tener acceso a las propiedades de columna y fila.
Los desarrolladores llevaron este concepto más allá y usaron propiedades adjuntas como una forma de comportamiento de componentes (por ejemplo, aquí está mi publicación sobre cómo hacer un GridView ordenable usando propiedades adjuntas escritas antes de que WPF incluyera un DataGrid). El enfoque ha sido reconocido como un patrón de diseño XAML llamado Comportamientos adjuntos .
Con suerte, esto proporcionó un poco más de información sobre por qué la herencia múltiple generalmente está mal vista.
fuente
Permitir la herencia múltiple hace que las reglas sobre sobrecargas de funciones y despacho virtual sean decididamente más difíciles, así como la implementación del lenguaje en torno a los diseños de objetos. Estos diseñadores / implementadores de lenguaje impactan bastante, y elevan el nivel ya alto para lograr un lenguaje estable, estable y adoptado.
Otro argumento común que he visto (y hecho a veces) es que al tener dos + clases base, su objeto casi siempre viola el Principio de responsabilidad única. O las dos clases base son buenas clases autónomas con su propia responsabilidad (causando la violación) o son tipos parciales / abstractos que trabajan entre sí para hacer una única responsabilidad cohesiva.
En este otro caso, tiene 3 escenarios:
Personalmente, creo que la herencia múltiple tiene una mala reputación, y que un sistema bien hecho de composición de estilo de rasgos sería realmente poderoso / útil ... pero hay muchas maneras en que puede implementarse mal, y muchas razones por las que es No es una buena idea en un lenguaje como C ++.
[editar] con respecto a su ejemplo, eso es absurdo. A Kia tiene electrónica. Se dispone de un motor. Del mismo modo, su electrónica tiene una fuente de alimentación, que resulta ser la batería de un automóvil. Herencia, y mucho menos herencia múltiple no tiene lugar allí.
fuente
La única razón por la que no se permite es porque facilita que las personas se disparen en el pie.
Lo que generalmente sigue en este tipo de discusión son los argumentos sobre si la flexibilidad de tener las herramientas es más importante que la seguridad de no tirar de su pie. No hay una respuesta decididamente correcta a ese argumento, porque como la mayoría de las otras cosas en la programación, la respuesta depende del contexto.
Si sus desarrolladores se sienten cómodos con MI y MI tiene sentido en el contexto de lo que está haciendo, entonces lo echará mucho de menos en un lenguaje que no lo admite. Al mismo tiempo, si el equipo no se siente cómodo con él, o no hay una necesidad real y la gente lo usa "solo porque puede", entonces eso es contraproducente.
Pero no, no existe un argumento absolutamente verdadero y convincente que pruebe que la herencia múltiple sea una mala idea.
EDITAR
Las respuestas a esta pregunta parecen ser unánimes. En aras de ser el defensor del diablo, proporcionaré un buen ejemplo de herencia múltiple, donde no hacerlo conduce a hacks.
Supongamos que está diseñando una aplicación para el mercado de capitales. Necesita un modelo de datos para valores. Algunos valores son productos de capital (acciones, fideicomisos de inversión inmobiliaria, etc.) otros son deuda (bonos, bonos corporativos), otros son derivados (opciones, futuros). Entonces, si está evitando el IM, creará un árbol de herencia muy claro y simple. Una acción heredará equidad, los bonos heredarán deuda. Genial hasta ahora, pero ¿qué pasa con los derivados? ¿Pueden basarse en productos similares a la renta variable o productos similares a los débitos? Ok, supongo que haremos que nuestro árbol de herencia se ramifique más. Tenga en cuenta que algunos derivados se basan en productos de renta variable, productos de deuda o ninguno de los dos. Entonces nuestro árbol de herencia se está complicando. Luego viene el analista de negocios y le dice que ahora apoyamos valores indexados (opciones de índice, opciones de índice futuro). Y estas cosas pueden basarse en la equidad, la deuda o los derivados. ¡Esto se está volviendo desordenado! ¿Mi opción futura de índice deriva Equity-> Stock-> Option-> Index? ¿Por qué no Equity-> Stock-> Index-> Option? ¿Qué pasa si un día encuentro ambos en mi código (Esto sucedió; historia real)?
El problema aquí es que estos tipos fundamentales se pueden mezclar en cualquier permutación que, naturalmente, no deriva uno del otro. Los objetos se definen por una relación, por lo que la composición no tiene ningún sentido. La herencia múltiple (o el concepto similar de mixins) es la única representación lógica aquí.
La solución real para este problema es tener los tipos Equity, Debt, Derivative, Index definidos y mezclados usando herencia múltiple para crear su modelo de datos. Esto creará objetos que tienen sentido y se prestan fácilmente para la reutilización del código.
fuente
Equity
yDebt
ambos implementanISecurity
.Derivative
Tiene unaISecurity
propiedad. Puede ser unISecurity
si es apropiado (no sé finanzas).IndexedSecurities
nuevamente contiene una propiedad de interfaz que se aplica a los tipos en los que se puede basar. Si son todosISecurity
, entonces todos tienenISecurity
propiedades y se pueden anidar arbitrariamente ...Las otras respuestas aquí parecen estar llegando principalmente a la teoría. Así que aquí hay un ejemplo concreto de Python, simplificado, en el que realmente me he estrellado, que requirió una buena cantidad de refactorización:
Bar
fue escrito asumiendo que tenía su propia implementación dezeta()
, que generalmente es una suposición bastante buena. Una subclase debería anularlo según corresponda para que haga lo correcto. Desafortunadamente, los nombres solo fueron casualmente iguales: hicieron cosas bastante diferentes, peroBar
ahora llamaban aFoo
la implementación:Es bastante frustrante cuando no se arrojan errores, la aplicación comienza a actuar de manera ligeramente incorrecta y el cambio de código que lo causó (la creación
Bar.zeta
) no parece ser el problema.fuente
super()
?super()
seBang
queBar, Foo
también podría solucionar el problema - pero su punto es buena, 1.Bar.zeta(self)
.Yo diría que no hay ningún problema real con MI en el idioma correcto . La clave es permitir estructuras de diamante, pero requieren que los subtipos proporcionen su propia anulación, en lugar de que el compilador elija una de las implementaciones basadas en alguna regla.
Hago esto en guayaba , un idioma en el que estoy trabajando. Una característica de Guava es que podemos invocar la implementación de un método de un supertipo específico. Por lo tanto, es fácil indicar qué implementación de supertipo se debe "heredar", sin ninguna sintaxis especial:
Si no proporcionáramos el
OrderedSet
suyotoString
, obtendríamos un error de compilación. No hay sorpresas.Encuentro que MI es particularmente útil con las colecciones. Por ejemplo, me gusta usar un
RandomlyEnumerableSequence
tipo para evitar declarargetEnumerator
matrices, deques, etc.:Si no tuviéramos MI, podríamos escribir un
RandomAccessEnumerator
para varias colecciones para usar, pero tener que escribir ungetEnumerator
método breve aún agrega repetitivo.Del mismo modo, MI es útil para heredar implementaciones estándar de
equals
,hashCode
ytoString
para colecciones.fuente
toString
el comportamiento de la persona, por lo que anular su comportamiento no viola el principio de sustitución. A veces también se obtienen "conflictos" entre métodos que tienen el mismo comportamiento pero diferentes algoritmos, especialmente con colecciones.Ordered extends Set and Sequence
unaDiamond
? Es solo una unión. Carece delhat
vértice. ¿Por qué lo llamas diamante? Pregunté aquí pero parece una pregunta tabú. ¿Cómo sabías que necesitas llamar a esta estructura triangular un Diamante en lugar de Unirte?Top
tipo implícito que declaratoString
, por lo que en realidad había una estructura de diamante. Pero creo que tiene un punto: una "unión" sin una estructura de diamante crea un problema similar, y la mayoría de los idiomas manejan ambos casos de la misma manera.La herencia, múltiple o no, no es tan importante. Si dos objetos de diferente tipo son sustituibles, eso es lo que importa, incluso si no están vinculados por herencia.
Una lista vinculada y una cadena de caracteres tienen poco en común, y no necesitan estar vinculados por herencia, pero es útil si puedo usar una
length
función para obtener el número de elementos en cualquiera de ellos.La herencia es un truco para evitar la implementación repetida de código. Si la herencia le ahorra trabajo, y la herencia múltiple le ahorra aún más trabajo en comparación con la herencia única, entonces esa es toda la justificación que se necesita.
Sospecho que algunos idiomas no implementan la herencia múltiple muy bien, y para los profesionales de esos idiomas, eso es lo que significa la herencia múltiple. Mencione la herencia múltiple a un programador de C ++, y lo que viene a la mente es algo acerca de los problemas cuando una clase termina con dos copias de una base a través de dos rutas de herencia diferentes, y si se usa
virtual
en una clase base, y la confusión sobre cómo se llaman los destructores , y así.En muchos idiomas, la herencia de clase se combina con la herencia de símbolos. Cuando deriva una clase D de una clase B, no solo está creando una relación de tipo, sino que debido a que estas clases también sirven como espacios de nombres léxicos, está tratando con la importación de símbolos del espacio de nombres B al espacio de nombres D, además de la semántica de lo que está sucediendo con los tipos B y D mismos. La herencia múltiple por lo tanto trae problemas de choque de símbolos. Si heredamos de
card_deck
ygraphic
, ambos "tienen" undraw
método, ¿qué significa paradraw
el objeto resultante? Un sistema de objetos que no tiene este problema es el de Common Lisp. Tal vez no sea coincidencia, la herencia múltiple se utiliza en los programas Lisp.Mal implementado, cualquier inconveniente (como la herencia múltiple) debe ser odiado.
fuente
length
operación es útil independientemente del concepto de herencia (que en este caso no ayuda: no es probable que podamos lograr nada al tratar de compartir la implementación entre los dos métodos de lalength
función). Sin embargo, podría haber alguna herencia abstracta: por ejemplo, tanto la lista como la cadena son de tipo secuencia (pero que no proporciona ninguna implementación).Por lo que puedo decir, parte del problema (además de hacer que su diseño sea un poco más difícil de entender (sin embargo, más fácil de codificar)) es que el compilador ahorrará suficiente espacio para los datos de su clase, lo que permite una gran cantidad de pérdida de memoria en el siguiente caso:
(Mi ejemplo podría no ser el mejor, pero tratar de entender el espacio de memoria múltiple para el mismo propósito, fue lo primero que se me ocurrió: P)
Concluya un DDD donde el perro de la clase se extiende desde caninus y pet, un caninus tiene una variable que indica la cantidad de alimento que debe comer (un número entero) bajo el nombre dietKg, pero una mascota también tiene otra variable para ese propósito, generalmente bajo el mismo nombre (a menos que establezca otro nombre de variable, entonces tendrá que codificar un código adicional, que era el problema inicial que quería evitar, para manejar y mantener la integridad de las variables bouth), entonces tendrá dos espacios de memoria para el exacto mismo propósito, para evitar esto, tendrá que modificar su compilador para reconocer ese nombre bajo el mismo espacio de nombres y simplemente asignar un solo espacio de memoria a esos datos, que desafortunadamente es imposible de determinar en el tiempo de compilación.
Podría, por supuesto, diseñar un lenguaje para especificar que dicha variable ya podría tener un espacio definido en otro lugar, pero al final el programador debería especificar dónde está ese espacio de memoria al que esta variable hace referencia (y nuevamente código adicional).
Confía en mí, la gente que está implementando este pensamiento realmente duro sobre todo esto, pero me alegra que lo hayas preguntado, tu amable perspectiva es la que cambia los paradigmas;) y considero esto, no digo que sea imposible (pero muchos supuestos y se debe implementar un compilador multifásico, y uno realmente complejo), solo digo que aún no existe, si comienza un proyecto para su propio compilador capaz de hacer "esto" (herencia múltiple), por favor hágamelo saber, yo Estaremos encantados de unirme a su equipo.
fuente
Durante bastante tiempo, nunca se me ocurrió qué tan totalmente diferentes son algunas tareas de programación de otras, y cuánto ayuda si los lenguajes y patrones utilizados se adaptan al espacio del problema.
Cuando está trabajando solo o aislado en el código que escribió, es un espacio de problemas completamente diferente de heredar una base de código de 40 personas en la India que trabajaron en ella durante un año antes de entregársela sin ninguna ayuda de transición.
Imagine que acaba de ser contratado por la compañía de sus sueños y luego heredó dicha base de código. Además, imagine que los consultores habían estado aprendiendo sobre (y por lo tanto enamorado de) la herencia y la herencia múltiple ... ¿Podría imaginar en qué podría estar trabajando?
Cuando hereda el código, la característica más importante es que es comprensible y las piezas están aisladas para que puedan trabajarse de forma independiente. Claro, cuando está escribiendo las estructuras de código, como la herencia múltiple, puede ahorrarle un poco de duplicación y parece ajustarse a su estado de ánimo lógico en ese momento, pero el siguiente tipo tiene más cosas para desenredar.
Cada interconexión en su código también hace que sea más difícil de entender y modificar piezas de forma independiente, doblemente con herencia múltiple.
Cuando trabajas como parte de un equipo, quieres apuntar al código más simple posible que no te proporciona absolutamente ninguna lógica redundante (eso es lo que realmente significa DRY, no es que no debas escribir mucho solo porque nunca tienes que cambiar tu código en 2 lugares para resolver un problema!)
Hay formas más sencillas de lograr el código DRY que la herencia múltiple, por lo que incluirlo en un idioma solo puede abrirlo a problemas insertados por otros que podrían no tener el mismo nivel de comprensión que usted. Incluso es tentador si su idioma no puede ofrecerle una forma simple / menos compleja de mantener su código SECO.
fuente
El mayor argumento en contra de la herencia múltiple es que se pueden proporcionar algunas habilidades útiles, y algunos axiomas útiles pueden mantenerse, en un marco que lo restringe severamente (*), pero no se puede proporcionar y / o mantener sin tales restricciones. Entre ellos:
La capacidad de tener múltiples módulos compilados por separado incluye clases que heredan de las clases de otros módulos, y recompila un módulo que contiene una clase base sin tener que recompilar cada módulo que hereda de esa clase base.
La capacidad de un tipo de heredar una implementación miembro de una clase primaria sin que el tipo derivado tenga que volver a implementarlo
El axioma de que cualquier instancia de objeto puede estar activada o desactivada directamente sobre sí misma o sobre cualquiera de sus tipos base, y tales activaciones y descargas, en cualquier combinación o secuencia, siempre preservan la identidad
El axioma de que si una clase derivada reemplaza y encadena a un miembro de clase base, el código de cadena invocará directamente al miembro base y no se invocará en ningún otro lugar.
(*) Normalmente, al exigir que los tipos que admiten herencia múltiple se declaren como "interfaces" en lugar de clases, y no permitir que las interfaces hagan todo lo que las clases normales pueden hacer.
Si se desea permitir la herencia múltiple generalizada, debe ceder algo más. Si X e Y heredan de B, ambos anulan el mismo miembro M y se encadenan a la implementación base, y si D hereda de X e Y pero no anula M, entonces dada una instancia q de tipo D, qué debería (( B) q) .M () hacer? No permitir tal conversión violaría el axioma que dice que cualquier objeto se puede convertir a cualquier tipo de base, pero cualquier comportamiento posible de la invocación de conversión y miembro violaría el axioma con respecto al encadenamiento de métodos. Se podría requerir que las clases solo se carguen en combinación con la versión particular de una clase base con la que se compilaron, pero eso a menudo es incómodo. Hacer que el tiempo de ejecución se niegue a cargar cualquier tipo en el que se pueda llegar a cualquier antepasado por más de una ruta podría ser viable, pero limitaría en gran medida la utilidad de la herencia múltiple. Permitir rutas de herencia compartidas solo cuando no existan conflictos crearía situaciones en las que una versión anterior de X sería compatible con una Y antigua o nueva, y una Y antigua sería compatible con una X antigua o nueva, pero la X nueva y la Y nueva ser compatible, incluso si ninguno de los dos hizo algo que en sí mismo debería ser un cambio radical.
Algunos lenguajes y marcos permiten la herencia múltiple, en la teoría de que lo que se obtiene del IM es más importante que lo que se debe renunciar para permitirlo. Sin embargo, los costos de MI son significativos y, en muchos casos, las interfaces proporcionan el 90% de los beneficios de MI a una pequeña fracción del costo.
fuente
FirstDerivedFoo
la anulación deBar
no hace nada más que encadenar a un no virtual protegidoFirstDerivedFoo_Bar
, ySecondDerivedFoo
anular cadenas a un no virtual protegidoSecondDerivedBar
, y si el código que desea acceder al método base usa esos métodos no virtuales protegidos, eso podría ser un enfoque viable ...base.VirtualMethod
) pero no conozco ningún soporte de idiomas para facilitar particularmente este enfoque. Desearía que hubiera una forma limpia de adjuntar un bloque de código tanto a un método protegido no virtual como a un método virtual con la misma firma sin requerir un método virtual de "una línea" (que termina tomando más de una línea en código fuente).