代码之家  ›  专栏  ›  技术社区  ›  Mecki

钻石问题真的能解决吗?

  •  14
  • Mecki  · 技术社区  · 16 年前

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

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

    [移除的车辆示例]

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


    更新:也许我的例子选得不好。

    看到这个图像

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

    当然,你可以使人在C++中是虚拟的,因此你只会有一个人在内存中的实例,但是真正的问题仍然存在。您将如何为GradTeachingFellow实现getDepartment()?考虑一下,他可能是一个系的学生,在另一个系教书。所以你要么返回一个部门,要么返回另一个部门;对于这个问题没有完美的解决方案,而且没有一个实现可以被继承(例如,学生和教师都可以是接口)这一事实对我来说似乎解决不了这个问题。

    17 回复  |  直到 13 年前
        1
  •  17
  •   Joris Timmermans    16 年前

    你看到的是违反 Liskov Substitution Principle 使之很难有一个工作的,逻辑的面向对象的结构。
    基本上,(公共)继承应该只缩小类的用途,而不是扩展类的用途。在这种情况下,通过继承两种类型的车辆,您实际上是在扩展用途,正如您所注意到的,它不起作用-对于水上车辆,移动应该与道路车辆非常不同。
    你可以把水上车辆和地面车辆的物体聚集在两栖车辆中,并从外部决定哪一个适合当前的情况。
    或者,您可以决定“vehicle”类是不必要的通用类,并且您将为这两个类分别提供接口。但是,这并不能解决两栖车辆本身的问题——如果在两个接口中都将移动方法称为“移动”,那么您仍然会遇到麻烦。所以我建议聚合而不是继承。

        2
  •  6
  •   Douglas Leeder    16 年前

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

    然而,可能发生的情况是两栖飞行器目标知道它目前是在水上还是在陆地上,并且做了正确的事情。

        3
  •  6
  •   Olivier    16 年前

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

    什么时候? GroundVehicle WaterVehicle 延伸 车辆 ,他们隐含地继承了本合同(类似: List.contains 从继承合同 Collection.contains --想象一下,如果它指定了不同的内容!).

    所以当混凝土 AmphibianVehicle 器具 MOVEL() ,它真正需要遵守的合同是 车辆 有一个钻石,但是合同没有改变,不管你是考虑钻石的一面还是另一面(或者我称之为设计问题)。

    如果您需要“移动”的契约来体现表面的概念,请不要将其定义为不模拟此概念的类型:

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

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

        4
  •  5
  •   Steven Evers    16 年前

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

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

        5
  •  4
  •   Patrick McDonald    16 年前

    我不知道Java,但是如果接口B和C从接口A继承,D类实现接口B和C,那么D类只实现一次移动方法,它是它应该实现的一个动作。正如您所说,编译器对此没有问题。

    从两栖车辆实施地面车辆和水上车辆的实例中,可以很容易地解决这一问题,例如,通过存储环境参考,以及暴露两栖车辆移动方法将要检查的环境表面特性。不需要将此作为参数传递。

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

        6
  •  4
  •   Clayton    16 年前

    基于接口的继承没有菱形问题。

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

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

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

        7
  •  3
  •   eljenso    16 年前

    如果我知道有两栖车 接口,继承 地面车辆和水上车辆,如何 我可以实现它的move()方法吗?

    您将提供适合 AmphibianVehicle S.

    如果A GroundVehicle 移动“不同”(即采用不同于 WaterVehicle ) 两栖车 继承了两种不同的方法,一种用于水上,一种用于地面。如果这不可能,那么 两栖车 不应该继承自 地面车辆 水上交通工具 .

    钻石问题一直是原因吗 糟糕的设计之类的 程序员和编译器都不需要 因为它不应该存在 首先?

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

        8
  •  2
  •   asdfwera    15 年前

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

    学生班和教师班用同一个名字把“系”的两个不同概念混为一谈。如果你想使用这种继承,你应该在教师中定义“getteachingdepartment”,在学生中定义“getresearchdepartment”。你的研究生既是老师又是学生,两者兼备。

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

        9
  •  1
  •   David Turvey    16 年前

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

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

    move()
    {
    
      if (this.isOnLand())
      {
         this.moveLikeLandVehicle();
      }
      else
      {
        this.moveLikeWaterVehicle();
      }
    }
    
        10
  •  1
  •   Kibbee    16 年前

    在这种情况下,将两栖车辆作为车辆的一个子类(水上车辆和陆上车辆的兄弟)可能是最有利的,以便在第一时间完全避免这一问题。不管怎样,这可能更正确,因为两栖车辆不是水上车辆或陆上车辆,它是完全不同的东西。

        11
  •  1
  •   ShuggyCoUk    16 年前

    如果move()基于它是地下水或水(而不是地面车辆和水上车辆接口本身扩展了具有move()签名的通用车辆接口),但预期您将混合和匹配地下水和水的实现者,那么您的示例之一实际上是一个设计不良的API。

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

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

    如果你有一件夹克,你希望它既是一件衣服,又容易毁坏,你将有一个名称冲突(合法命名)的穿着方法。

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

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

    这种合理的、可能的名称冲突是很难得到的,Java没有被嘲笑为无法提供明确的接口实现,这表明这个问题对于现实世界的使用来说不是一个重要的问题。

        12
  •  0
  •   willoller    16 年前

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

    在两栖车辆的情况下,呼叫者(我们称为“油门”)可能不知道水/地面的状态,但是一个中间决定对象(如“变速箱”)和“牵引力控制”可能会解决这个问题,然后用适当的参数move(车轮)或move(道具)调用move()。

        13
  •  0
  •   p2u    16 年前

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

        14
  •  0
  •   Victor Rodrigues    16 年前

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

        15
  •  0
  •   Michel    16 年前

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

        16
  •  0
  •   Peter Mortensen icecrime    15 年前

    实际上,如果 Student Teacher 都是接口,它实际上解决了你的问题。如果它们是接口,那么 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();
      }
    }
    

    没关系 年级学生 概念上没有“有”老师和学生-封装仍然是一种方式。

        17
  •  0
  •   Nirupma    14 年前

    接口A { 空格加法(); }

    接口B扩展A { 空格加法(); }

    接口C扩展了 { 空格加法(); }

    D类实现B、C {

    }

    这不是钻石问题吗?

    推荐文章