关于克服Java中缺乏多重继承的任何既定实践



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

   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,反之亦然是不可持续的。是否有任何既定的最佳实践可以将两者的共享代码放在一个地方,尽管不允许多重继承?

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

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

面向对象的基本原理之一是

比起继承,更喜欢组合

我想说的是你根本不需要接口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没有扩展或实现任何内容。

这就是我要说的——比起继承,更喜欢组合

在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 start()方法,因为在一天结束时,无论我们的车里有多少台机器(计算机),我们仍然只想驾驶它。

这只是一个例子,尽管这个例子可能有点奇怪,但我希望它能帮助理解实现默认方法的意义。

示例源的更新:

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

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

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

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

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

如果您想进一步了解组合的优势,请参阅有效Java中的本章:支持组合而非继承

最新更新