¿Hay alguna razón "real" por la que se odie la herencia múltiple?

123

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?

  1. 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.

  2. Porque es un electrónico. Hay cables, tableros de circuitos, interruptores, etc. arriba y abajo de esa cosa.

  3. 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.

Panzercrisis
fuente
24
también fomenta la herencia sobre la composición ... (cuando debería ser al revés)
fanático del trinquete
34
"Puede arruinarlo" es una razón perfectamente válida para eliminar una función. El diseño del lenguaje moderno está algo fragmentado en ese punto, pero generalmente puede clasificar los idiomas en "Poder de restricción" y "Poder de flexibilidad". Ninguno de los dos es "correcto", no creo, ambos tienen puntos fuertes que hacer. MI es una de esas cosas que se usa para Evil la mayoría de las veces y, por lo tanto, los lenguajes restrictivos lo eliminan. Los flexibles no lo hacen, porque "la mayoría de las veces" no es "literalmente siempre". Dicho esto, creo que los mixins / rasgos son una mejor solución para el caso general.
Phoshi
8
Existen alternativas más seguras a la herencia múltiple en varios idiomas, que van más allá de las interfaces. Echa un vistazo a Scala 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.
KChaloux
55
Vea también esta pregunta anteriormente cerrada: programmers.stackexchange.com/questions/122480/…
Doc Brown
19
KiaSpectrano es una Electronic ; que tiene Electrónica, y puede ser una ElectronicCar(lo que extendería Car...)
Brian S

Respuestas:

68

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.

var rectangle = new Square();
rectangle.length= 5;
rectangle.width= 6;
Assert.AreEqual(30, rectangle.GetArea()); 
//Square returns 36 because setting the width clobbers the length

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.

Michael Brown
fuente
14
"Hombre, realmente necesito MI para hacer esto correctamente", mira mi respuesta para ver un ejemplo. En pocas palabras, los conceptos ortogonales que satisfacen una relación es-una tienen sentido para MI. Son muy raros, pero existen.
MrFox
13
Hombre, odio ese ejemplo de rectángulo cuadrado. Hay muchos ejemplos más esclarecedores que no requieren mutabilidad.
asmeurer
99
Me encanta que la respuesta para "proporcionar un ejemplo real" fue discutir una criatura ficticia. Excelente ironía +1
jjathman
3
Entonces está diciendo que la herencia múltiple es mala, porque la herencia es mala (sus ejemplos son todos sobre herencia de interfaz única). Por lo tanto, basándose solo en esta respuesta, un lenguaje debe deshacerse de la herencia y las interfaces, o tener herencia múltiple.
ctrl-alt-delor
12
No creo que estos ejemplos realmente funcionen ... nadie dice que un pegaso es un pájaro y un caballo ... usted dice que es un animal con alas y un animal con pezuñas. El ejemplo de cuadrado y rectángulo tiene un poco más de sentido porque te encuentras con un objeto que se comporta de manera diferente de lo que pensarías en función de su definición afirmada. Sin embargo, creo que esta situación sería culpa de los programadores por no captar esto (si de hecho esto resultó ser un problema).
Richard
60

¿Hay algo que no estoy viendo aquí?

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:

  1. A no sabe nada sobre B - Genial, puedes combinar las clases porque tienes suerte.
  2. A sabe acerca de B - ¿Por qué A no solo heredó de B?
  3. A y B se conocen: ¿por qué no hiciste una sola clase? ¿Qué beneficio viene de hacer estas cosas tan acopladas pero parcialmente reemplazables?

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í.

Telastyn
fuente
8
Otra pregunta interesante es cómo se inicializan las clases base + sus clases base. Encapsulación> Herencia.
jozefg
"un sistema bien hecho de composición de estilo de rasgos sería realmente poderoso / útil ..." - Estos se conocen como mixins , y son poderosos / útiles. Los mixins se pueden implementar usando MI, pero no se requiere herencia múltiple para los mixins. Algunos idiomas admiten mixins inherentemente, sin MI.
BlueRaja - Danny Pflughoeft
Para Java en particular, ya tenemos múltiples interfaces e INVOKEINTERFACE, por lo que MI no tendría implicaciones obvias de rendimiento.
Daniel Lubarov
Tetastyn, estoy de acuerdo en que el ejemplo fue malo y no sirvió a su pregunta. La herencia no es para adjetivos (electrónicos), es para sustantivos (vehículo).
Richard
29

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.

Señor Fox
fuente
27
He trabajado en finanzas. Hay una razón por la cual se prefiere la programación funcional sobre OO en el software financiero. Y lo has señalado. MI no soluciona este problema, lo exacerba. Créame, no puedo decirle cuántas veces he escuchado "Es así ... excepto" cuando trato con clientes financieros. Eso, excepto, se convierte en la ruina de mi existencia.
Michael Brown
8
Esto me parece muy solucionable con interfaces ... de hecho, parece más adecuado para interfaces que cualquier otra cosa. Equityy Debtambos implementan ISecurity. DerivativeTiene una ISecuritypropiedad. Puede ser un ISecuritysi es apropiado (no sé finanzas). IndexedSecuritiesnuevamente contiene una propiedad de interfaz que se aplica a los tipos en los que se puede basar. Si son todos ISecurity, entonces todos tienen ISecuritypropiedades y se pueden anidar arbitrariamente ...
Bobson
11
@Bobson, el problema radica en que debe volver a implementar la interfaz para cada implementador y, en muchos casos, ES lo mismo. Puede usar la delegación / composición pero luego pierde el acceso a los miembros privados. Y dado que Java no tiene delegados / lambdas ... literalmente no hay una buena manera de hacerlo. Excepto posiblemente con una herramienta Aspect como Aspect4J
Michael Brown
10
@Bobson exactamente lo que dijo Mike Brown. Sí, puede diseñar una solución con interfaces, pero será torpe. Sin embargo, su intuición para usar interfaces es muy correcta, es un deseo oculto de mixins / herencia múltiple :).
MrFox
11
Esto toca una rareza en la que los programadores de OO prefieren pensar en términos de herencia y a menudo no aceptan la delegación como un enfoque de OO válido, uno que típicamente produce una solución más ágil, más sostenible y extensible. Después de haber trabajado en finanzas y atención médica, he visto el desorden inmanejable que MI puede crear, especialmente a medida que las leyes de impuestos y salud cambian año tras año y la definición de un objeto EL ÚLTIMO año no es válida para ESTE año (pero aún debe cumplir la función de cualquier año , y debe ser intercambiable). La composición produce un código más ágil que es más fácil de probar y cuesta menos con el tiempo.
Shaun Wilson
11

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:

class Foo(object):
   def zeta(self):
      print "foozeta"

class Bar(object):
   def zeta(self):
      print "barzeta"

   def barstuff(self):
      print "barstuff"
      self.zeta()

class Bang(Foo, Bar):
   def stuff(self):
      self.zeta()
      print "---"
      self.barstuff()

z = Bang()
z.stuff()

Barfue escrito asumiendo que tenía su propia implementación de zeta(), 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, pero Barahora llamaban a Foola implementación:

foozeta
---
barstuff
foozeta

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.

Izkata
fuente
¿Cómo se evita el problema del diamante a través de llamadas a super()?
Panzercrisis
@Panzercrisis Lo siento, no importa esa parte. Recordé mal a qué problema se refiere generalmente el "problema del diamante": Python tenía un problema separado causado por la herencia en forma de diamante que super()se
resolvió
55
Me alegro de que el MI de C ++ esté mucho mejor diseñado. El error anterior simplemente no es posible. Tienes que desambiguarlo manualmente antes de que el código incluso se compile.
Thomas Eding
2
Cambiar el orden de herencia en Bangque Bar, Footambién podría solucionar el problema - pero su punto es buena, 1.
Sean Vieira
1
@ThomasEding Usted puede eliminar la ambigüedad de forma manual sigue: Bar.zeta(self).
Tyler Crompton
9

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:

type Sequence[+A] {
  String toString() {
    return "[" + ... + "]";
  }
}

type Set[+A] {
  String toString() {
    return "{" + ... + "}";
  }
}

type OrderedSet[+A] extends Sequence[A], Set[A] {
  String toString() {
    // This is Guava's syntax for statically invoking instance methods
    return Set.toString(this);
  }
}

Si no proporcionáramos el OrderedSetsuyo toString, 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 RandomlyEnumerableSequencetipo para evitar declarar getEnumeratormatrices, deques, etc.:

type Enumerable[+A] {
  Source[A] getEnumerator();
}

type Sequence[+A] extends Enumerable[A] {
  A get(Int index);
}

type RandomlyEnumerableSequence[+A] extends Sequence[A] {
  Source[A] getEnumerator() {
    ...
  }
}

type DynamicArray[A] extends MutableStack[A],
                             RandomlyEnumerableSequence[A] {
  // No need to define getEnumerator.
}

Si no tuviéramos MI, podríamos escribir un RandomAccessEnumeratorpara varias colecciones para usar, pero tener que escribir un getEnumeratormétodo breve aún agrega repetitivo.

Del mismo modo, MI es útil para heredar implementaciones estándar de equals, hashCodey toStringpara colecciones.

Daniel Lubarov
fuente
1
@AndyzSmith, entiendo su punto, pero no todos los conflictos de herencia son problemas semánticos reales. Considere mi ejemplo: ninguno de los tipos hace promesas sobre toStringel 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.
Daniel Lubarov
1
@Asmageddon estuvo de acuerdo, mis ejemplos involucran solo la funcionalidad. ¿Cuáles ves como las ventajas de los mixins? Al menos en Scala, los rasgos son esencialmente clases abstractas que permiten MI: todavía se pueden usar para la identidad y aún provocan el problema del diamante. ¿Hay otros idiomas donde los mixins tienen diferencias más interesantes?
Daniel Lubarov
1
Técnicamente, las clases abstractas se pueden usar como interfaces, para mí, el beneficio es simplemente el hecho de que la construcción en sí misma es un "consejo de uso", lo que significa que sé qué esperar cuando veo el código. Dicho esto, actualmente estoy codificando en Lua, que no tiene un modelo OOP inherente: los sistemas de clases existentes comúnmente permiten incluir tablas como mixins, y el que estoy codificando usa clases como mixins. La única diferencia es que solo puede usar herencia (única) para identidad, mixins para funcionalidad (sin información de identidad) e interfaces para verificaciones adicionales. Quizás de alguna manera no sea mucho mejor, pero tiene sentido para mí.
Llamageddon
2
¿Por qué se llama a la Ordered extends Set and Sequenceuna Diamond? Es solo una unión. Carece del hatvé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?
Val
1
@RecognizeEvilasWaste ese lenguaje tiene un Toptipo implícito que declara toString, 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.
Daniel Lubarov
7

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 lengthfunció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 virtualen 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_decky graphic, ambos "tienen" un drawmétodo, ¿qué significa para drawel 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.

Kaz
fuente
2
Su ejemplo con el método length () de lista y contenedores de cadenas es malo, ya que estos dos están haciendo cosas completamente diferentes. El propósito de la herencia no es reducir la repetición del código. Si desea reducir la repetición de código, refactorice partes comunes en una nueva clase.
Bћовић
1
@ BЈовић Los dos no están haciendo cosas "completamente" diferentes en absoluto.
Kaz
1
Compartiendo cadena y lista , se puede ver mucha repetición. Usar la herencia para implementar estos métodos comunes simplemente estaría mal.
Bћовић
1
@ BЈовић No se dice en la respuesta que una cadena y una lista deben estar vinculadas por herencia; todo lo contrario. El comportamiento de poder sustituir una lista o una cadena en una lengthoperació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 la lengthfunció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).
Kaz
2
Además, la herencia es una forma de refactorizar partes a una clase común: es decir, la clase base.
Kaz
1

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.

Ordiel
fuente
1
¿En qué punto podría un compilador suponer que las variables del mismo nombre de diferentes clases primarias son seguras de combinar? ¿Qué garantía tiene de que caninus.diet y pet.diet realmente sirven para el mismo propósito? ¿Qué harías con funciones / métodos con el mismo nombre?
Mr.Mindor
jajaja estoy totalmente de acuerdo con usted @ Mr.Mindor eso es exactamente lo que digo en mi respuesta, la única forma en que se me ocurre acercarme a ese enfoque es la programación orientada a prototipos, pero no digo que sea imposible , y esa es exactamente la razón por la que dije que muchos supuestos deben hacerse durante el tiempo de compilación, y algunos "datos" / especificaciones adicionales deben ser escritos por el programador (sin embargo, es más codificación, que era el problema original)
Ordiel
-1

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.

Bill K
fuente
Además, imagine que los consultores habían estado aprendiendo : he visto muchos lenguajes que no son MI (por ejemplo, .net) en los que los consultores se volvieron locos con DI e IoC basados ​​en interfaz para hacer que todo sea un desastre impenetrablemente complicado. MI no empeora esto, probablemente lo mejora.
gbjbaanb
@gbjbaanb Tienes razón, es fácil para alguien que no se ha quemado estropear ninguna función; de hecho, es casi un requisito para aprender una función que la estropees para ver cómo usarla mejor. Tengo curiosidad porque parece que estás diciendo que no tener MI fuerza más DI / IoC que no he visto, no creo que el código de consultor que obtuviste con todas las interfaces / DI malas hubiera sido mejor también tener un árbol de herencia de muchos a muchos, pero podría ser mi inexperiencia con MI. ¿Tiene una página que podría leer sobre el reemplazo de DI / IoC con MI?
Bill K
-3

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.

Super gato
fuente
¿Reclamarían los votantes negativos hacer comentarios? Creo que mi respuesta proporciona información que no está presente en otros idiomas, con respecto a las ventajas semánticas que se pueden recibir al rechazar la herencia múltiple. El hecho de que permitir la herencia múltiple requeriría renunciar a otras cosas útiles sería una buena razón para que cualquiera que valore esas otras cosas más que la herencia múltiple se oponga a MI.
supercat
2
Los problemas con MI se resuelven o evitan fácilmente usando las mismas técnicas que usaría con una sola herencia. Ninguna de las habilidades / axiomas que ha mencionado está inherentemente obstaculizada por MI, excepto la segunda ... y si está heredando de dos clases que proporcionan diferentes comportamientos para el mismo método, de todos modos debe resolver esa ambigüedad. Pero si tiene que hacer eso, probablemente ya esté haciendo un mal uso de la herencia.
cHao
@cHao: si se requiere que las clases derivadas deben anular los métodos principales y no encadenarse a ellos (la misma regla que las interfaces) se pueden evitar los problemas, pero esa es una limitación bastante severa. Si uno usa un patrón donde FirstDerivedFoola anulación de Barno hace nada más que encadenar a un no virtual protegido FirstDerivedFoo_Bar, y SecondDerivedFooanular cadenas a un no virtual protegido SecondDerivedBar, 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 ...
supercat
... (evitando la sintaxis 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).
supercat
No existe un requisito inherente en MI de que las clases no llamen a métodos básicos, y no hay una razón particular por la que sea necesaria. Parece un poco extraño culpar a MI, teniendo en cuenta que el problema también afecta a SI.
cHao