【问题标题】:Any established practices on overcoming the lack of multiple inheritance in Java?任何克服 Java 中缺乏多重继承的既定实践?
【发布时间】:2015-03-21 06:16:12
【问题描述】:

我有一个经典的钻石继承问题

   A
 /   \
B     C
 \   /
   D

都是接口,我有

AImpl(A)
|       \
|        \
BImpl(B)  CImpl(C)
|          \
|           \
DImpl(B,C)   \
|             F(C)
|
E(B,C)

其中类E 实现了接口BC,但F 只实现了接口C

由于缺少多重继承,我目前在DImplCImpl 中有重复的功能。

我刚刚修复了CImpl 中的一个错误,但忘记为DImpl 做同样的事情。显然,记住始终将代码从 CImpl 复制到 DImpl,反之亦然,因为代码库不断增长,因此不太可持续。尽管不允许多重继承,是否有任何既定的最佳实践可以将两者的共享代码放在一个地方?

编辑 -- 多重继承的解决方案是让DImpl继承CImpl.cFunction(),而不是将DImpl.cFunction重新定义为CImpl.cFunction的副本

EDIT 2 -- 示例代码:

public interface Animal {
  public void eat();
}

public interface FlyingAnimal extends Animal {
  public void fly();
}

public interface RunningAnimal extends Animal {
  public void run();
}

public interface Monster extends FlyingAnimal, RunningAnimal {
  public void roar();
}

public class AnimalImpl implements Animal {
  @Override
  public void eat() {
    ...
  }
}

public class FlyingAnimalImpl extends AnimalImpl implements FlyingAnimal {
  @Override
  public void fly() {
    ...
  }
}

public class RunningAnimalImpl extends AnimalImpl implements RunningAnimal {
  @Override
  public void run() {
    ...
  }
}

public class MonsterImpl extends FlyingAnimalImpl implements Monster {
  @Override
  public void run() {
    ...
  }

  @Override
  public void roar() {
    ...
  }
}

public class ScaryMonster extends MonsterImpl implements Monster {
  public void sneakAround() {
    ...
  }
}

public class Human extends RunningAnimalImpl implements RunningAnimal {
  public void scream() {
    ...
  }
}

现在,如果我在 RunningAnimalImpl.run() 中发现错误并修复它,我必须记住将修复复制到 MonsterImpl.run()

【问题讨论】:

  • 什么是 A、B、C 和 D?
  • 你确定你的层次结构正确吗?用这些通用名称是不可能的。如果有多重继承,你的解决方案是什么?

标签: java multiple-inheritance diamond-problem


【解决方案1】:

这是一个非常糟糕的设计。首先,您不应该仅仅因为它在接口中是可能的就具有菱形结构。

OOP的基本原理之一是

更喜欢组合而不是继承!

我的意思是你根本不需要接口 D。无论您需要在哪里使用DImpl,只需提供对interface A 的引用,然后根据您的运行时需要将BImlCImpl 实例传递给它。这样,您只需更改BImpl 代码或CImpl 代码即可修复错误,它将在您今天使用DImpl 实例的任何地方使用。

根据您的评论,代码将类似于 -

public class ScaryMonster {
  Animal animal;
  public  ScaryMonster(Animal animal) {
      this.animal = animal;
  }

  public void fly() {
      if(animal instanceof FlyingAnimal ) {
        ((FlyingAnimal )animal).fly();
      }
      else {
        throw new Exception("This mosnter cannot fly");
      }     
  }

  public void run() {
    if(animal instanceof RunningAnimal ) {
        ((RunningAnimal )animal).run();
      }
      else {
        throw new Exception("This mosnter cannot run");
      }
  }

  public void sneakAround() {
    ...
  }
}

如果你想让你的怪物既飞又跑,将MonsterImpl 的实例传递给构造函数。现在注意ScaryMonster 没有扩展或实现任何东西。

这就是我要说的——组合优于继承!

【讨论】:

  • 好吧,DImpl 仍然需要为那些只想使用 C 对象的类实现 interface C。虽然你是对的,我可能可以在 DImpl 中创建一个内部 CImpl 对象并将我的调用传递给该对象
  • 我是说你根本不需要DDImpl。无论您在哪里使用 DimplD ,请将其替换为 A 引用,并在运行时传递 BImplCImpl 并调用相应的方法。
  • 你能给我看一个使用我上面例子中的类的例子吗?最终我确实想要一个ScaryMonster sm = new ScaryMonster();,我可以毫不费力地同时调用sm.fly()sm.run()
  • 已添加代码 sn-p 以便您更好地理解这一点。很多设计模式都使用这个。
  • 是的,但问题仍然存在,我如何不在MonsterImpl.run()RunningAnimalImpl.run() 之间重复代码?我需要先构造一个MonsterImpl,然后再将它传递给ScaryMonster的构造函数
【解决方案2】:

在 Java 8 中,您可以在接口中实现默认方法,因此如果您有一个具有通用实现的接口,只需在接口内部定义它们,并在需要对其进行一些更改时覆盖它们。当然,这是假设您使用的是 Java 8。

例如:

public interface A {
    default void cFunction(){
       System.out.println("Calling A.cFunction");
    }
}

public class DImpl implements A {
}

DImpl 可以调用 cFunction,它会默认调用接口实现。

如果两个接口有一个签名相同的方法,您可以通过引用接口名称和方法来调用它们,例如A.super.cFunction()

更有意义的例子:

public interface Driveable {
    default void start(Vehicle vehicle){
       System.out.println("Starting my driveable thing");
       vehicle.mileage++;
    }
}


public interface Machine {
    default void start(){
       System.out.println("Starting my machine");
    }
}



public class ElectricCar extends Vehicle implements Driveable, Machine {
    public void start(){
       Driveable.super.start(this);
    }
}

public class DIYCar extends Vehicle implements Driveable, Machine {
    public void start(){
        System.out.println("instant fire");
    }
}

如您所见,您可以在接口中实现默认方法并在具体类中使用它。在这种情况下,ElectriCar 是 Driveable 和 Machine,但我们希望它使用 Driveable start() 方法,因为归根结底,无论我们的车里有多少机器(计算机),我们仍然只想驾驶它。

这只是一个例子,虽然这个例子可能有点奇怪,但我希望它有助于理解能够实现默认方法。

更新您的示例来源:

在您的 Monster 和 Animal 能够运行的情况下,您应该有一个 RunnableCreature 接口,其实现为run()。这样,如果 Monster 和 Animal 运行相同,它们可以引用默认的 run() 方法,否则它可以覆盖它并实现自己的。

如果你需要你的默认方法来操作变量,你的两个(或更多)类具有相同的 run() 方法将具有共同的属性,因此应该有一个共同的基类。您可以将此基类传递给默认方法并根据需要操作其变量。

【讨论】:

  • 很好的答案,当然可以理解!但是如果函数需要修改内部变量(例如调用RunningAnimal.run()会消耗能量或者调用Machine.start()boolean machineIsOn;设置为true),那么这种方法是行不通的,对吧?
  • 您可以使用方法参数传入您要操作的基础对象。例如,如果您的所有 RunnableCreatures 都是生物,您可以将 Creature 作为参数传递:run(Creature c){ c.energy--; }
  • 我已经更新了我的答案,展示了如何将自己的实例传递给操作变量的方法。希望它能让您对可能的选择有所了解。
【解决方案3】:

不要在 D 中实现 C,而不是在 D 中组合继承。这样您就不会重复代码,并且只有一个 C 实现。

如果您以某种方式需要从 C 继承(我认为您当时需要审查设计),那么我仍然建议您编写 C 并在 D 中实现 C 的所有方法并通过组合对象委托调用。

【讨论】:

    【解决方案4】:

    一种常见的替代方法是使用组合而不是继承。说D B 并且也是 C 是否有意义? D 真的需要暴露BC 中包含的每个 public 方法吗?如果这些问题的答案是否定的,那么继承就不是适合这项工作的工具。

    如果您想进一步了解组合的优点,请查看 Effective Java 中的这一章:Favor composition over inheritance

    【讨论】:

    • 我相信这两个问题的答案都是肯定的,但很高兴知道,谢谢!
    猜你喜欢
    • 2011-09-16
    • 1970-01-01
    • 2017-06-04
    • 2017-05-17
    • 2013-01-07
    • 2016-03-28
    • 2012-03-17
    • 1970-01-01
    • 2017-06-15
    相关资源
    最近更新 更多