2009-02-18 36 views
14

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?¿Se puede resolver realmente el problema del diamante?

Las personas afirman 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. ver más abajo:

[ejemplo de vehículo eliminado]

es un problema de diamantes siempre la causa del mal diseño de la clase y algo que ni programador ni compilador necesita para resolver, ya que no debe existir en primer lugar?


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

Ver esta imagen

Diamond Problem http://cartan.cas.suffolk.edu/oopdocbook/opensource/src/multinheritance/PersonStudentTeacher.png

Por supuesto, puede hacer que la persona virtual en C++ y, por tanto, sólo tendrá una instancia de la persona en la memoria, pero el verdadero problema 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 para el problema y el hecho de que ninguna implementación pueda ser heredada (por ejemplo, Student y Teacher podrían ser ambas interfaces) no parece resolver el problema para mí.

+2

Eso es más una publicación de blog que una pregunta. Tristemente, parece que la mayoría de las "respuestas" aquí hasta ahora no han leído todo. Pero es una perspectiva interesante que presentas. – Ken

Respuesta

16

Lo que está viendo es cómo las violaciones de Liskov Substitution Principle hacen que sea realmente difícil tener una estructura lógica orientada a objetos que funcione.
Básicamente, la herencia (pública) solo debe restringir 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, puede 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.

+0

Buen punto. Pero, ¿la herencia múltiple no siempre viola este principio, ya que heredar de dos clases siempre hace que la clase infantil sea "más amplia"? – Mecki

+0

Si las dos interfaces son completamente ortogonales (es decir, no se aplican al mismo concepto), entonces puede salirse con la suya. PERO ... entonces violas el principio de responsabilidad única, que es otro mundo de dolor. –

+0

"restringir el propósito de la clase" se llama especialización :) – leppie

5

La gente afirma Java no conoce el problema de diamante. 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í, ya que el control de la implementación de la interfaz en D. La firma del método es el mismo entre ambas interfaces (B/C), y ya que no tienen interfaces de aplicación - no hay problemas.

+0

Ver arriba: solo porque move no tiene implementación no resuelve el problema, aún no puedo implementar move() en la clase D de tal manera que funcione. – Mecki

+0

Entonces permítanme reformular: si decide implementar un patrón de diamante con interfaces, entonces es su problema decidir CÓMO implementarlo. Sin embargo, no hay nada implícitamente equivocado al hacerlo. –

+0

Por esa lógica, tampoco hay un problema de diamante en MI: simplemente especifique el orden, si el valor predeterminado no es deseable. Esto es como interfaces SI +, excepto que no tiene que escribir el despachador usted mismo. – Ken

4

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 que debería implementar. Como dices, el compilador no tiene ningún problema con esto.

Según el ejemplo que da sobre AmphibianVehicle que implementa GroundVehicle and WaterVehicle, esto podría resolverse fácilmente almacenando una referencia al entorno, por ejemplo, y exponiendo una propiedad de superficie en el entorno que el método Move de AmphibianVehicle inspeccionaría. 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".

0

que se dan cuenta de que este es un caso específico, no una solución general, pero suena como que necesita un sistema adicional para determinar el estado y decidir en 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/suelo, sino un objeto intermedio como "transmisión" junto con " control de tracción "podría resolverlo, luego invocar move() con el movimiento de parámetro adecuado (ruedas) o mover (prop).

6

C# tiene explicit interface implementation para ocuparse parcialmente de 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 agua o tierra, y hace lo correcto .

4

No hay un problema de diamante con la herencia basada en la interfaz.

Con la herencia basada en clases, las múltiples clases extendidas pueden tener diferentes implementaciones de un método, por lo que hay 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.

+0

Para el compilador, pero yo, como programador, debo decidir cómo implementarlo. Entonces, en lugar de tener clases abstractas, se podría decir que la herencia múltiple es buena y si hay ambigüedad, ninguna de las dos se hereda, el programador debe sobrescribir el método. – Mecki

+0

Creo que o bien deberías poder escribir el método de manera que haga lo correcto en ambos casos, o has cometido un error al tratar de combinar las clases de esa manera; quizás necesites moveLand() y moveWater (), o algún otro enfoque. – rmeador

0

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.

1

No creo que la prevención de la herencia múltiple de hormigón 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 utilizar este método de decisión sobre el movimiento. Esto preservará la interfaz sin parámetros.

move() 
{ 

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

Si sé que tiene una interfaz AmphibianVehicle , que hereda de GroundVehicle y WaterVehicle, cómo habría que implementarlo método move() es?

Proporcionar la implementación adecuada para AmphibianVehicle s.

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

es un problema de diamantes siempre la causa de diseño de clase malo y algo ni programador ni compilador necesita de resolver, ya que no debería existir en el 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.

0

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.

+0

El problema del diamante todavía existe hasta cierto punto en java para importar bibliotecas. Está parcialmente resuelto por la calificación de nombre completo, pero existe la posibilidad de que el código que ya haya escrito utilizando la clase X pueda volverse ambiguo mediante la importación de otra biblioteca que defina una clase X. –

1

En este caso, probablemente sería más ventajoso tener AmphibiousVehicle como una subclase de Vehicle (hermano de WaterVehicle and 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.

+0

Si bien esta es una buena solución, significa que debe reescribir todo el código que escribiste para las otras dos clases secundarias que en realidad funcionaría bastante bien para AmphibiousVehicle también ... buena solución, pero destruye la idea detrás de la herencia de implementación algo – Mecki

+0

Solo si mucha de la funcionalidad en el vehículo anfibio es la misma como el vehículo de agua y el vehículo terrestre. Si algo hereda de 2 objetos que son casi completamente opuestos, la mayoría de ellos probablemente tengan que ser reescritos de todos modos. – Kibbee

+0

Creo que semánticamente es más correcto hacerlo al estilo Diamante. Esto es especialmente claro si WaterVehicle y LandVehicle implementan algunos métodos no heredados directamente del vehículo (como isSinking o inTrafficJam). AmphibiousVehicle debería ser capaz de hacer ambas cosas. – Jordi

6

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

Cuando GroundVehicle y WaterVehicleVehicle se extienden, de forma implícita que heredan este contrato (analogía: List.contains hereda su contrato de Collection.contains - imaginar si se especifica algo diferente!).

Entonces cuando el hormigón AmphibianVehicle implementa move(), el contrato que realmente necesita respetar es Vehicle 's. 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 el contrato de "movimiento" para encarnar la noción de superficie, no definirlo en un tipo que no modela esta noción:

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

(analogía: get(int) 's contrato está definido por la interfaz List.No podría ser definido por Collection, como colecciones no están necesariamente ordenados)

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

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

El único problema que veo la hora de implementar múltiples interfaces es cuando dos los 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.

+0

Me gusta tu ejemplo con Vehicle and GraphicalComponent, no es un problema real de diamantes, pero este es también uno de los grandes problemas que puedes tener con interfaces, donde el hecho de que no se hereda ninguna implementación realmente no te salva el día ;-) – Mecki

+0

Genial ¡responder! Aunque hubiera sido bueno si hubiera dicho explícitamente que el problema del diamante no es realmente tan grande y que permitir el MI con interfaces realmente no ahorra muchos problemas. – Jordi

1

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 tierra y implementadores de agua, entonces su ejemplo uno realmente es uno de una API pobremente diseñada.

El problema real 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 usted tiene una chaqueta que desea ser a la vez una prenda de vestir, y destructibles que tendrá una colisión nombre en la (legítimamente nombrado) desgaste método.

Java no tiene 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 explicit interface implementations mediante el cual una clase puede definir dos métodos del 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 la variable conocida en tiempo de compilación (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 explícitas de interfaz sugeriría que el problema no es significativo para el uso en el mundo real.

+0

Java no tiene un problema con esto. Una clase Java que implementa Destructible and Garment tendrá 3 métodos, Wear(), Rip() y Desrobe(). El hecho de que "wear" puede tener 2 significados en inglés es un problema de nombre, no de Java. Wear() hará lo que sea que lo codifique, independientemente del nombre. – Clayton

+0

Exactamente, pero el desgaste no puede determinar si se lo invoca en el contexto de que se le solicite que lo use o si se degrada. Esto a pesar del hecho de que tal información podría haber estado disponible para el sistema de tipos en compilación. Por lo tanto, no puede hacer ambas cosas de manera efectiva. – ShuggyCoUk

+0

Deseos conflictivos debido a una colisión de nombre que no se puede resolver * es * el problema del diamante. Si puede incluir información para resolverlo, deja de ser un problema y, por definición, ya no es un diamante. – ShuggyCoUk

0

El problema del diamante en C++ ya está resuelto: use la 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.

0

En realidad, si Student y Teacher son ambas interfaces, de hecho resuelve su problema. Si son interfaces, entonces getDepartment es simplemente un método que debe aparecer en su clase GradTeachingFellow. El hecho de que las interfaces Student y 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 GradStudent enseñando/siendo un TA en un departamento, y siendo un estudiante en otro. La encapsulación es probablemente lo que usted quiere 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 significa conceptualmente "tienen" un maestro y el estudiante - encapsulación sigue siendo el camino a seguir.

+0

¿Y cómo resuelve esto el problema, que GradTeachingFellow puede ser un alumno en un departamento, pero un maestro en otro? Incluso si se trata de una interfaz, fallará, pero no en el nivel del código. – Mecki

+0

Tiene razón, resuelve el problema en el nivel del código, pero no en el nivel semántico. En el caso de que está hablando, puede que la herencia no sea la que desea aquí; quizás quiera encapsularla. Esto no funcionaría en Java, pero alternativamente, sería semánticamente posible heredar ambos métodos bajo nombres con alias, p. Ej. getTeachingDepartment() y getStudentDepartment(). Java no es un lenguaje muy flexible, por lo que la encapsulación es probablemente la mejor opción para una clase GradStudent. –

2

El problema que está viendo en el ejemplo de Estudiante/Profesor 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" utilizando 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 postgrado, incluso este modelo es probablemente insuficiente.

+0

Probablemente tengas razón; el problema es que las interfaces como esta tienden a crecer con el tiempo y quien creó la interfaz del Profesor podría ser una persona diferente a la que creó la interfaz del Estudiante, y ninguno ha considerado que pueda haber una clase con ambas interfaces en al mismo tiempo. Tal vez una tercera persona creó la clase GradTeachingFellow y eso antes de que Student o Teacher tuvieran un método de departamento. – Mecki

+0

Lo que estoy tratando de decir aquí es que en un lenguaje de OO ideal, diferentes personas hacen diferentes clases e, idealmente, sin importar cómo se ve la jerarquía de herencia o cómo se alteran esas clases, nada se rompería alguna vez. Esto está lejos de la realidad, sin embargo. La herencia múltiple en mi humilde opinión solo funciona de maravilla si: 1. hay herencia más allá del alcance de una biblioteca/binario; 2. Todos los que trabajan en la biblioteca/binario tienen acceso al código fuente y permiso para cambiar todo, si así lo desea. Si alguno de los dos no es cierto, es mejor quedarse con la herencia individual (mucho menos dolor de cabeza). – Mecki

0

interfaz A { void add(); }

interfaz B extiende A { void add(); }

interfaz C extiende A { void add(); }

clase D implementa B, C {

}

no es problema de diamante.

+2

No, porque esas son interfaces. – BoltClock

Cuestiones relacionadas