有没有关于克服 Java 中缺乏多重继承的既定做法?

Any established practices on overcoming the lack of multiple inheritance in Java?

我有一个 classic 钻石继承问题

   A
 /   \
B     C
 \   /
   D

都是接口,我有

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

其中 class E 实现接口 BC,但 F 仅实现接口 C.

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

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

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

编辑 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()

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

例如:

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

public class DImpl implements A {
}

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

如果 2 个接口有一个具有相同签名的方法,您可以通过引用接口名称和方法来调用它们,例如 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");
    }
}

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

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

更新您的示例来源:

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

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

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

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

一个常见的替代方法是使用组合而不是继承。说 D 是一个 B 并且 也是一个 C 有意义吗? D 真的需要公开 BC 中包含的 every public 方法吗?如果以上任何一个问题的答案是否定的,那么继承就不是完成这项工作的正确工具。

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

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

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 没有扩展或实现任何东西。

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