ventajas que programacion problemas orientada objetos múltiple multiple herencia datos concepto ambiguedad design-patterns oop anti-patterns

design patterns - que - ¿Se puede resolver realmente el problema del diamante?



herencia multiple ventajas (17)

Un problema típico en la programación de OO es el problema de los diamantes. Tengo una clase padre A con dos subclases B y C. A tiene un método abstracto, B y C lo implementan. Ahora tengo una subclase D, que hereda de B y C. El problema del diamante es ahora, ¿qué implementación usará D, la de B o la de C?

La gente dice que Java no conoce ningún problema con los diamantes. Solo puedo tener herencia múltiple con interfaces y dado que no tienen implementación, no tengo ningún problema con los diamantes. ¿Es esto realmente cierto? No lo creo. Vea abajo:

[ejemplo de vehículo eliminado]

¿Es un problema de diamante siempre la causa del diseño de clase malo y algo que ni el programador ni el compilador necesitan resolver, porque no debería existir en primer lugar?

Actualización: Tal vez mi ejemplo fue mal elegido.

Ver esta imagen

Problema Diamante http://cartan.cas.suffolk.edu/oopdocbook/opensource/src/multinheritance/PersonStudentTeacher.png

Por supuesto, puede hacer que la persona sea virtual en C ++ y, por lo tanto, solo tendrá una instancia de persona en la memoria, pero el problema real persiste en mi humilde opinión. ¿Cómo implementaría getDepartment () para GradTeachingFellow? Considere, él podría ser estudiante en un departamento y enseñar en otro. Entonces puede devolver un departamento u otro; no hay una solución perfecta al problema y el hecho de que ninguna implementación pueda ser heredada (p. ej., el alumno y el profesor pueden ser interfaces) no parece resolver el problema.


interfaz A {void add (); }

la interfaz B extiende A {void add (); }

la interfaz C amplía A {void add (); }

clase D implementa B, C {

}

¿No es un problema de diamantes?


La gente dice que Java no conoce ningún problema con los diamantes. Solo puedo tener herencia múltiple con interfaces y dado que no tienen implementación, no tengo ningún problema con los diamantes. ¿Es esto realmente cierto?

sí, porque usted controla la implementación de la interfaz en D. La firma del método es la misma entre ambas interfaces (B / C), y las interfaces no tienen implementación, no hay problemas.


Si sé que tengo una interfaz AmphibianVehicle, que hereda de GroundVehicle y WaterVehicle, ¿cómo implementaré su método move ()?

Debería proporcionar la implementación adecuada para AmphibianVehicle s.

Si un GroundVehicle mueve "de manera diferente" (es decir, toma diferentes parámetros que un WaterVehicle ), entonces el WaterVehicle AmphibianVehicle hereda dos métodos diferentes, uno para el agua y otro para el suelo. Si esto no es posible, AmphibianVehicle no debe heredar de GroundVehicle y WaterVehicle .

¿Es un problema de diamante siempre la causa del diseño de clase malo y algo que ni el programador ni el compilador necesitan resolver, porque no debería existir en primer lugar?

Si se debe al mal diseño de la clase, es el programador el que debe resolverlo, ya que el compilador no sabría cómo hacerlo.


El problema del diamante en C ++ ya está resuelto: use herencia virtual. O mejor aún, no seas perezoso y heredes cuando no es necesario (o inevitable). En cuanto al ejemplo que dio, esto podría resolverse redefiniendo lo que significa ser capaz de conducir en el suelo o en el agua. ¿La capacidad de moverse a través del agua realmente define un vehículo a base de agua o es simplemente algo que el vehículo puede hacer? Prefiero pensar que la función mover () que describió tiene algún tipo de lógica que pregunta "¿dónde estoy y puedo mudarme aquí?" El equivalente de una función bool canMove() que depende del estado actual y las capacidades inherentes del vehículo. Y no necesitas herencia múltiple para resolver ese problema. Solo use un mixin que responda la pregunta de diferentes maneras dependiendo de lo que sea posible y tome la superclase como un parámetro de plantilla para que la función canMove virtual sea visible a través de la cadena de herencia.


El problema realmente existe En la muestra, AmphibianVehicle-Class necesita otra información: la superficie. Mi solución preferida es agregar un método getter / setter en la clase AmpibianVehicle para cambiar el miembro de la superficie (enumeración). La implementación ahora podría hacer lo correcto y la clase permanecer encapsulada.


En este caso, probablemente sería más ventajoso tener AmphibiousVehicle como una subclase de Vehicle (hermano de WaterVehicle y LandVehicle), para evitar completamente el problema en primer lugar. Probablemente sea más correcto de todos modos, ya que un vehículo anfibio no es un vehículo acuático o un vehículo terrestre, es algo completamente diferente.


En su ejemplo, move() pertenece a la interfaz del Vehicle y define el contrato "yendo del punto A al punto B".

Cuando GroundVehicle y WaterVehicle extienden Vehicle , heredan implícitamente este contrato (analogy: List.contains hereda su contrato de Collection.contains - imagine si especifica algo diferente!).

Entonces, cuando el Vehicle AmphibianVehicle concreto se move() , el contrato que realmente debe respetar es el del Vehicle . Hay un diamante, pero el contrato no cambia si se considera un lado del diamante o el otro (o yo llamaría a eso un problema de diseño).

Si necesita que el contrato de "movimiento" incorpore la noción de superficie, no la defina en un tipo que no modele esta noción:

public interface GroundVehicle extends Vehicle { void ride(); } public interface WaterVehicle extends Vehicle { void sail(); }

(analogy: el contrato get(int) está definido por la interfaz List . No podría ser definido por Collection , ya que las colecciones no están necesariamente ordenadas)

O refactorice su interfaz genérica para agregar la noción:

public interface Vehicle { void move(Surface s) throws UnsupportedSurfaceException; }

El único problema que veo al implementar múltiples interfaces es cuando dos métodos de interfaces totalmente no relacionadas chocan:

public interface Vehicle { void move(); } public interface GraphicalComponent { void move(); // move the graphical component on a screen } // Used in a graphical program to manage a fleet of vehicles: public class Car implements Vehicle, GraphicalComponent { void move() { // ??? } }

Pero eso no sería un diamante. Más como un triángulo invertido.


Lo que está viendo es cómo las violaciones del Principio de sustitución de Liskov hacen que sea realmente difícil tener una estructura lógica orientada a objetos que funcione.
Básicamente, la herencia (pública) solo debe estrechar el propósito de la clase, no extenderla. En este caso, heredando dos tipos de vehículos, de hecho estás ampliando el propósito, y como habrás notado, no funciona: el movimiento debería ser muy diferente para un vehículo acuático que para un vehículo de carretera.
En su lugar, podría agregar un vehículo acuático y un objeto de vehículo terrestre en su vehículo anfibio y decidir externamente cuál de los dos será apropiado para la situación actual.
Alternativamente, podría decidir que la clase "vehículo" es innecesariamente genérica y tendrá interfaces separadas para ambos. Sin embargo, eso no resuelve solo el problema de tu vehículo anfibio: si llamas al método de movimiento "moverse" en ambas interfaces, seguirás teniendo problemas. Así que sugeriría la agregación en lugar de la herencia.


Me doy cuenta de que esta es una instancia específica, no una solución general, pero parece que necesita un sistema adicional para determinar el estado y decidir qué tipo de movimiento () el vehículo llevaría a cabo.

Parece que en el caso de un vehículo anfibio, el que llama (digamos "acelerador") no tendría idea del estado del agua / tierra, pero un objeto intermedio como "transmisión" junto con "control de tracción" podría descárgalo, luego llama a move () con el movimiento de parámetro apropiado (ruedas) o mueve (prop).


No creo que evitar la herencia múltiple concreta traslade el problema del compilador al programador. En el ejemplo que usted dio, aún sería necesario que el programador especificara al compilador qué implementación usar. No hay forma de que el compilador adivine cuál es la correcta.

Para su clase de anfibios, puede agregar un método para determinar si el vehículo está en agua o tierra y usar este método de decisión sobre el movimiento para usar. Esto preservará la interfaz sin parámetros.

move() { if (this.isOnLand()) { this.moveLikeLandVehicle(); } else { this.moveLikeWaterVehicle(); } }


No hay un problema de diamantes con herencia basada en interfaz.

Con la herencia basada en clases, las múltiples clases extendidas pueden tener diferentes implementaciones de un método, por lo que existe ambigüedad en cuanto a qué método se usa realmente en tiempo de ejecución.

Con la herencia basada en la interfaz solo hay una implementación del método, por lo que no hay ambigüedad.

EDITAR: En realidad, lo mismo se aplicaría a la herencia basada en la clase para los métodos declarados como Resumen en la superclase.


No sé Java, pero si las interfaces B y C heredan de la interfaz A, y la clase D implementa las interfaces B y C, entonces la clase D simplemente implementa el método de movimiento una vez, y es A.Move lo que debe implementar. Como dices, el compilador no tiene ningún problema con esto.

A partir del ejemplo que da sobre AmphibianVehicle que implementa GroundVehicle y WaterVehicle, esto podría resolverse fácilmente almacenando una referencia a Environment, por ejemplo, y exponiendo una Surface Property on Environment que inspeccionaría el método Move de AmphibianVehicle. No es necesario pasar esto como un parámetro.

Tiene razón en el sentido de que es algo que el programador debe resolver, pero al menos compila y no debería ser un "problema".


Puede tener el problema de diamante en C ++ (que permite herencia múltiple), pero no en Java o en C #. No hay forma de heredar de dos clases. Implementar dos interfaces con el mismo método de declaración no implica en esta situación, ya que la implementación del método concreto solo puede realizarse en la clase.


Si move () tiene diferencias semánticas basadas en Ground o Water (en lugar de GroundVehicle y WaterVehicle interfaces ambas extendiendo interfaz GeneralVehicle que tiene la firma move ()) pero se espera que mezcle y combine implementos de tierra y agua, entonces su el ejemplo uno es realmente uno de una API pobremente diseñada.

El verdadero problema es cuando la colisión del nombre es, efectivamente, accidental. por ejemplo ( muy sintético):

interface Destructible { void Wear(); void Rip(); } interface Garment { void Wear(); void Disrobe(); }

Si tiene una chaqueta que desea tanto ser una prenda como destructible, tendrá una colisión de nombre en el método de desgaste (legítimamente nombrado).

Java no tiene una solución para esto (lo mismo es cierto para muchos otros lenguajes estáticos). Los lenguajes de programación dinámica tendrán un problema similar, incluso sin el diamante o la herencia, es solo una colisión de nombre (un problema potencial inherente con Duck Typing).

.Net tiene el concepto de implementaciones explícitas de interfaz mediante el cual una clase puede definir dos métodos con el mismo nombre y firma siempre que ambos estén marcados en dos interfaces diferentes. La determinación del método relevante para llamar se basa en la interfaz conocida de tiempo de compilación de la variable (o si se refleja por la elección explícita del destinatario)

Que las colisiones de nombres razonables y probables son tan difíciles de obtener y que java no ha sido ridiculizado como inutilizable por no proporcionar las implementaciones de interfaces explícitas sugeriría que el problema no es significativo para el uso en el mundo real.


C# tiene una implementación de interfaz explícita para lidiar parcialmente con esto. Al menos en el caso de que tenga una de las interfaces intermedias (un objeto de la misma ...)

Sin embargo, lo que probablemente sucede es que el objeto AmphibianVehicle sabe si está actualmente en el agua o en tierra, y hace lo correcto.


El problema que está viendo en el ejemplo de Estudiante / Maestro es simplemente que su modelo de datos es incorrecto, o al menos insuficiente.

Las clases de Estudiante y Profesor combinan dos conceptos diferentes de "departamento" al usar el mismo nombre para cada uno de ellos. Si desea utilizar este tipo de herencia, en su lugar debe definir algo como "getTeachingDepartment" en Teacher y "getResearchDepartment" en Student. Su GradStudent, que es a la vez Maestro y Estudiante, implementa ambos.

Por supuesto, dadas las realidades de la escuela de posgrado, incluso este modelo es probablemente insuficiente.


En realidad, si Student y Teacher son ambas interfaces, de hecho resuelve tu problema. Si son interfaces, entonces getDepartment es simplemente un método que debe aparecer en su clase GradTeachingFellow . El hecho de que las interfaces entre el Student y el Teacher hagan cumplir esa interfaz no es un conflicto en absoluto. Implementar getDepartment en su clase GradTeachingFellow satify ambas interfaces sin ningún problema de diamante.

PERO, como se señala en un comentario, esto no resuelve el problema de una enseñanza de GradStudent / ser un TA en un departamento, y ser un estudiante en otro. La encapsulación es probablemente lo que quieres aquí:

public class Student { String getDepartment() { return "Economics"; } } public class Teacher { String getDepartment() { return "Computer Engineering"; } } public class GradStudent { Student learning; Teacher teaching; public String getDepartment() { return leraning.getDepartment()+" and "+teaching.getDepartment(); // or some such } public String getLearningDepartment() { return leraning.getDepartment(); } public String getTeachingDepartment() { return teaching.getDepartment(); } }

No importa que un GradStudent no conceptualmente "tenga" un maestro y un alumno: la encapsulación sigue siendo el camino a seguir.