钻石问题能真正解决吗?

问题描述 投票:14回答:17

OO编程中的典型问题是钻石问题。我有父类A,有两个子类B和C.A有一个抽象方法,B和C实现它。现在我有一个子类D,它继承了B和C.钻石问题现在,D使用什么实现,B或C之一?

人们声称Java不知道钻石问题。我只能有接口的多重继承,因为它们没有实现,我没有钻石问题。这是真的吗?我不这么认为。见下文:

[删除车辆示例]

钻石问题总是导致糟糕的类设计,也不是程序员和编译器需要解决的问题,因为它首先不应该存在?


更新:也许我的榜样选择不当。

看到这个图像

Diamond Problem (来源:suffolk.edu

当然你可以用C ++创建Person虚拟,因此你只有一个人在内存中的实例,但真正的问题仍然存在恕我直言。你如何为GradTeachingFellow实现getDepartment()?考虑一下,他可能是一个系的学生,另一个系教学。所以你可以退回一个部门或另一个部门;对于这个问题没有完美的解决方案,并且没有实现可以继承(例如学生和教师都可以作为接口)似乎并没有解决问题。

design-patterns oop anti-patterns
17个回答
17
投票

您所看到的是如何违反Liskov Substitution Principle使得拥有一个有效的,逻辑的面向对象结构变得非常困难。 基本上,(公共)继承应该只缩小类的目的,而不是扩展它。在这种情况下,通过继承两种类型的车辆,您实际上是在扩展目的,并且正如您所注意到的那样,它不起作用 - 水上交通工具的移动应该与公路交通工具的移动非常不同。 您可以在水陆两用车辆中聚集水上交通工具和地面车辆物体,并在外部决定哪两个适合当前情况。 或者你可以决定“车辆”类是不必要的通用,你将有两个单独的接口。这并不能解决你自己的两栖车辆的问题 - 如果你在两个界面中调用移动方法“移动”,你仍然会遇到麻烦。所以我建议聚合而不是继承。


1
投票

在这种情况下,将两栖车辆作为车辆的子类(WaterVehicle和LandVehicle的兄弟)可能是最有利的,以便首先完全避免这个问题。无论如何,它可能更加正确,因为两栖车辆不是水上交通工具或陆地车辆,它完全是另一回事。


1
投票

如果move()具有基于它的Ground或Water的语义差异(而不是GroundVehicle和WaterVehicle接口本身都扩展了具有move()签名的GeneralVehicle接口)但是预计你将混合和匹配地面和水实施者然后你的例如,一个是设计糟糕的api之一。

真正的问题是,名称冲突实际上是偶然的。例如(非常合成):

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

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

如果你有一件夹克,你希望它既是一件衣服又可以破坏,你就会在(合法命名的)磨损方法上发生名称冲突。

Java没有解决方案(其他几种静态类型语言也是如此)。动态编程语言会有类似的问题,即使没有钻石或继承,它只是名称冲突(鸭子打字的固有潜在问题)。

.Net具有explicit interface implementations的概念,其中类可以定义两个具有相同名称和签名的方法,只要它们都标记为两个不同的接口即可。确定调用的相关方法是基于变量的编译时间已知接口(或者如果通过被调用者的显式选择反射)

这种合理的,可能的名称冲突很难得到,并且java没有因为不提供显式接口实现而无法使用,这表明该问题对于现实世界的使用来说并不重要。


0
投票

我意识到这是一个特定的实例,而不是一般的解决方案,但听起来你需要一个额外的系统来确定状态并决定车辆将执行哪种移动()。

似乎在两栖车辆的情况下,呼叫者(比如说“油门”)不知道水/地的状态,但是像“传动”这样的中间确定对象与“牵引力控制”一起可能弄明白,然后使用适当的参数move(wheels)或move(prop)调用move()。


0
投票

问题确实存在。在样本中,AmphibianVehicle-Class需要另一个信息 - 表面。我首选的解决方案是在AmpibianVehicle类上添加一个getter / setter方法来更改表面成员(枚举)。实现现在可以做正确的事情,并且类保持封装。


0
投票

您可以在C ++(允许多重继承)中使用菱形问题,但不能在Java或C#中使用。没有办法继承两个类。在这种情况下,实现具有相同方法声明的两个接口并不意味着,因为具体的方法实现只能在类中进行。


0
投票

C ++中的钻石问题已经解决:使用虚拟继承。或者更好的是,当没有必要(或不可避免)时,不要懒惰和继承。至于你给出的例子,这可以通过重新定义能够在地面或水中行驶的意义来解决。通过水的能力是否真的定义了水基车辆或者只是车辆能够做到的事情?我宁愿认为你描述的move()函数背后有一些逻辑,它询问“我在哪里,我能在这里移动吗?”相当于bool canMove()函数取决于当前状态和车辆的固有能力。而且您不需要多重继承来解决该问题。只需使用mixin以不同的方式回答问题,具体取决于可能的情况,并将超类作为模板参数,这样虚拟的canMove函数将通过继承链可见。


0
投票

实际上,如果StudentTeacher都是接口,它实际上解决了你的问题。如果它们是接口,那么getDepartment只是一个必须出现在GradTeachingFellow类中的方法。 StudentTeacher接口强制执行该接口的事实根本不是冲突。在你的getDepartment类中实现GradTeachingFellow可以满足两个接口而不会出现任何钻石问题。

但是,正如评论中所指出的,这并不能解决GradStudent教学/在一个部门担任TA,在另一个部门担任学生的问题。封装可能是你想要的:

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();
  }
}

这并不重要,因为GradStudent在概念上并没有“拥有”老师和学生 - 封装仍然是要走的路。


0
投票

interface A {void add(); }

接口B扩展A {void add(); }

接口C扩展A {void add(); }

D类实现B,C {

}

不是钻石问题。


6
投票

C#explicit interface implementation部分处理这个问题。至少在你有一个中间接口(它的一个对象......)的情况下

然而,可能发生的事情是AmphibianVehicle对象知道它目前是在水上还是陆地上,并做正确的事情。


6
投票

在您的示例中,move()属于Vehicle接口,并定义了“从A点到B点”的契约。

GroundVehicleWaterVehicle扩展Vehicle时,他们隐含地继承了这个合同(类比:List.containsCollection.contains继承了它的合同 - 想象它是否指定了不同的东西!)。

因此,当混凝土AmphibianVehicle实施move()时,它真正需要尊重的合同是Vehicle的。有一颗钻石,但无论你是考虑钻石的一面还是另一面(或者我称之为设计问题),合同都不会改变。

如果您需要“移动”的合同来体现表面的概念,请不要将其定义为不对此概念进行建模的类型:

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

(比喻:get(int)的契约是由List界面定义的。它不可能由Collection定义,因为集合不一定是有序的)

或者重构您的通用接口以添加概念:

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

我在实现多个接口时遇到的唯一问题是来自完全不相关的接口的两个方法碰巧碰撞:

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() {
        // ???
    }
}

但那不会是钻石。更像是一个颠倒的三角形。


5
投票

人们声称Java不知道钻石问题。我只能有接口的多重继承,因为它们没有实现,我没有钻石问题。这是真的吗?

是的,因为你在D中控制接口的实现。两个接口(B / C)之间的方法签名是相同的,并且看到接口没有实现 - 没有问题。


4
投票

我不了解Java,但是如果接口B和C继承自接口A,而类D实现接口B和C,则类D只实现移动方法一次,它是A.Move它应该实现。如你所说,编译器对此没有任何问题。

从你给出的关于实现GroundVehicle和WaterVehicle的AmphibianVehicle的例子中,这可以通过存储对Environment的引用,例如,并在环境中暴露AmphibianVehicle的Move方法将检查的Surface属性来轻松解决。不需要将其作为参数传递。

你是正确的,它是程序员要解决的东西,但至少它编译,不应该是一个“问题”。


4
投票

基于接口的继承没有钻石问题。

使用基于类的继承,多个扩展类可以具有不同的方法实现,因此在运行时实际使用哪个方法存在歧义。

使用基于接口的继承,该方法只有一个实现,因此没有歧义。

编辑:实际上,对于在超类中声明为Abstract的方法,同样适用于基于类的继承。


3
投票

如果我知道有一个AmphibianVehicle接口,继承了GroundVehicle和WaterVehicle,我将如何实现它的move()方法?

您将提供适用于AmphibianVehicles的实现。

如果GroundVehicle“以不同方式”移动(即采用与WaterVehicle不同的参数),那么AmphibianVehicle继承了两种不同的方法,一种用于水,一种用于地面。如果这是不可能的,那么AmphibianVehicle不应该继承GroundVehicleWaterVehicle

钻石问题总是导致糟糕的类设计,也不是程序员和编译器需要解决的问题,因为它首先不应该存在?

如果是由于糟糕的类设计,程序员需要解决它,因为编译器不知道如何解决它。


2
投票

您在学生/教师示例中看到的问题仅仅是您的数据模型是错误的,或者至少是不够的。

学生和教师课程通过对每个部门使用相同的名称来混淆两个不同的“部门”概念。如果你想使用这种继承,你应该在Teacher中定义类似“getTeachingDepartment”的内容,在Student中定义“getResearchDepartment”。您的GradStudent既是教师又是学生,同时实现这两者。

当然,考虑到研究生院的现实,即使这种模式也可能不足。


1
投票

我不认为防止具体的多重继承将问题从编译器转移到程序员。在您给出的示例中,程序员仍然需要向编译器指定要使用的实现。编译器无法猜出哪个是正确的。

对于您的两栖类,您可以添加一种方法来确定车辆是在水上还是陆地上,并使用此方法决定使用的移动方法。这将保留无参数接口。

move()
{

  if (this.isOnLand())
  {
     this.moveLikeLandVehicle();
  }
  else
  {
    this.moveLikeWaterVehicle();
  }
}
© www.soinside.com 2019 - 2024. All rights reserved.