17

OO 编程中的一个典型问题是菱形问题。我有两个子类 B 和 C 的父类 A。A 有一个抽象方法,B 和 C 实现它。现在我有一个子类 D,它继承了 BC。菱形问题现在是,D 应该使用什么实现,B 的一个还是 C 的一个?

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

[移除车辆示例]

钻石问题是否总是导致糟糕的类设计以及程序员和编译器都不需要解决的问题,因为它本来就不应该存在?


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

看到这张图片

钻石问题
(来源:suffolk.edu

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

4

18 回答 18

20

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

于 2009-02-18T16:14:55.297 回答
6

C#明确的接口实现来部分处理这个。至少在您拥有其中一个中间接口(其中一个对象..)的情况下

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

于 2009-02-18T16:14:46.670 回答
6

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

GroundVehicleWaterVehicle扩展时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;
}

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

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

但这不会是钻石。更像是一个倒三角形。

于 2009-02-18T18:01:24.323 回答
5

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

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

于 2009-02-18T16:12:56.330 回答
4

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

从您给出的有关实现 GroundVehicle 和 WaterVehicle 的 AmphibianVehicle 的示例中,可以通过存储对 Environment 的引用来轻松解决这个问题,例如,并在 Environment 上公开 AmphibianVehicle 的 Move 方法将检查的 Surface 属性。无需将此作为参数传递。

从某种意义上说,您是对的,这是程序员要解决的问题,但至少它可以编译并且不应该是“问题”。

于 2009-02-18T16:13:27.140 回答
4

基于接口的继承不存在钻石问题。

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

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

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

于 2009-02-18T16:15:55.357 回答
3

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

您将提供适合AmphibianVehicles 的实现。

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

钻石问题是否总是导致糟糕的类设计以及程序员和编译器都不需要解决的问题,因为它本来就不应该存在?

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

于 2009-02-18T16:21:11.350 回答
3

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

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

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

于 2010-04-26T23:07:42.497 回答
1

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

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

move()
{

  if (this.isOnLand())
  {
     this.moveLikeLandVehicle();
  }
  else
  {
    this.moveLikeWaterVehicle();
  }
}
于 2009-02-18T16:20:58.713 回答
1

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

于 2009-02-18T16:30:15.323 回答
1

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

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

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

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

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

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

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

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

于 2009-02-18T19:33:49.220 回答
0

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

似乎在两栖车辆的情况下,调用者(比如说“油门”)不知道水/地面的状态,但是像“传输”这样的中间确定对象与“牵引力控制”相结合可能弄清楚,然后使用正确的参数 move(wheels) 或 move(prop) 调用 move()。

于 2009-02-18T16:14:44.487 回答
0

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

于 2009-02-18T16:16:30.427 回答
0

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

于 2009-02-18T16:23:12.610 回答
0

C++ 中的菱形问题已经解决:使用虚拟继承。或者更好的是,不要偷懒,在不必要(或不可避免)时继承。至于你给出的例子,这可以通过重新定义能够在地面或水中驾驶的含义来解决。在水中移动的能力真的定义了水基车辆还是仅仅是车辆能够做的事情?我宁愿认为你描述的 move() 函数背后有某种逻辑,询问“我在哪里,我真的可以搬到这里吗?” 相当于一个bool canMove()功能取决于当前状态和车辆的固有能力。你不需要多重继承来解决这个问题。只需使用一个 mixin,它会根据可能的情况以不同的方式回答问题,并将超类作为模板参数,因此虚拟 canMove 函数将通过继承链可见。

于 2009-02-18T20:13:32.290 回答
0

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

但是,正如评论中指出的那样,这并不能解决一个部门的GradStudent教学/助教,以及另一个部门的学生的问题。封装可能是您想要的:

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- 封装仍然是要走的路。

于 2010-04-26T07:49:08.463 回答
0

接口 A { void add(); }

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

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

D类实现B,C {

}

是不是钻石问题。

于 2011-02-07T07:35:45.767 回答
0

组合优于继承

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

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

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

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

进一步的实践思考

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

  • 几种语言(例如Go)如果模棱两可(类似于 this.Student.getDepartment),则需要基本类型,从思维方式来看,这已经朝着组合的方向发展(超过继承)。

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

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

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

于 2021-07-02T10:11:16.713 回答