钻石问题能真正解决吗?

时间:2009-02-18 16:04:44

标签: design-patterns oop anti-patterns

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

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

[移除车辆示例]

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


更新:也许我的例子选择不当。

见图片

Diamond Problem
(来源:suffolk.edu

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

18 个答案:

答案 0 :(得分:17)

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

答案 1 :(得分:6)

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

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

答案 2 :(得分:6)

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

GroundVehicleWaterVehicle {延伸{1}},它们隐含地继承此合同(比喻:Vehicle继承List.contains其合同 - 如果指定的话它想象不同的东西!)。

因此,当具体Collection.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;
}

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

答案 3 :(得分:5)

  

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

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

答案 4 :(得分:4)

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

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

你是正确的,这是程序员要解决的问题,但至少它是编译而不应该是'问题'。

答案 5 :(得分:4)

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

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

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

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

答案 6 :(得分:3)

  

如果我知道有两栖车辆   接口,继承   GroundVehicle和WaterVehicle,怎么样   我会实现它的move()方法吗?

您将提供适合AmphibianVehicle的实现。

如果GroundVehicle以“不同方式”移动(即采用与WaterVehicle不同的参数),则AmphibianVehicle会继承两种不同的方法,一种用于水上,一种用于地面。如果无法做到这一点,则AmphibianVehicle不应继承GroundVehicleWaterVehicle

  

钻石问题始终是原因   坏班设计和什么的   程序员和编译器都不需要   解决,因为它不应该存在   首先是什么?

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

答案 7 :(得分:2)

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

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

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

答案 8 :(得分:1)

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

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

move()
{

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

答案 9 :(得分:1)

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

答案 10 :(得分: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没有因为不提供显式接口实现而无法使用,这表明该问题对于现实世界的使用来说并不重要。

答案 11 :(得分:0)

组合优于继承

(答案太多了,但因为还没有出现)

通常这些类型的问题,最突出的是“致命的死亡钻石”是通过使用组合来解决/绕过的(class a has x, y, z) 而不是继承(class a is x, y, z)。

是否已解决/与什么有关?

  • 您是对的,关于您的设计,这并不能解决多继承问题。一种能够为子类(或子接口)“重新标记”基本方法的语言可能会解决这个问题(至少对于直接调用,当向下转换时这仍然会存在问题;请参阅进一步的其他尝试)。
  • 关于(Java)语言,这实际上已经解决了。编译器(甚至开发人员)很清楚,两个接口方法(同名和签名)只能通过一个方法实现。

进一步的实践思考

尽管如此,两种情况在使用组合时都可以解决,但这可能有点不方便,例如在 Java 中,因为(在那里)您不能直接使用 mySpecificGradTeachingStudent.getName()

  • 如果有歧义(类似于 this.Student.getDepartment),几种语言(例如 Go)需要一个基类型,这从思想上来说已经朝着组合的方向发展(继承)。

  • 类似地,其他语言(例如 Rust)更进一步,使用所谓的 traits 代替(完全消除继承)。意思是结构实现了 TraitATraitB,代替了“成为 A 和/或 B”。

  • 因为所有这些想法基本上都建立在“接口概念”和(简化的)组合中添加语法糖(恕我直言,这确实是要走的路,并且也解决了其他问题,例如对象在几次之后完全改变继承层),这是最后一次(非常)不同的尝试。任何(多继承)语言都可以简单地添加优先级的概念。这意味着(例如)实现的第一个基本组件/类将是提供其方法堆栈的那个(即 Student),而其他方法将被简单地忽略。这仍然会引发诸如以下问题:“当被专门向下转换为 Teacher 对象时会发生什么,但即使是这些情况也可以通过约定来判断。(警告:这可能不是最直观的从设计的角度来看解决方案,但对于语言规范来说仍然是一个简单的解决方案。)

最后,参考学生/教师示例的一些想法(与龙一样的小偏差):https://dev.to/thisismahmoud/composition-over-inheritance-4fn9

答案 12 :(得分:0)

界面A. {   void add(); }

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

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

D类实现B,C {

}

不是钻石问题。

答案 13 :(得分: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概念上没有“拥有”老师和学生 - 封装仍然是可行的方法。

答案 14 :(得分:0)

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

答案 15 :(得分:0)

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

答案 16 :(得分:0)

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

答案 17 :(得分:0)

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

似乎在两栖车辆的情况下,来电者(比如说“油门”)不知道水/地的状态,而是一个中间决定对象,如“变速器”和“牵引力”控制“可能会弄清楚,然后用适当的参数移动(轮子)或移动(道具)调用move()。