我有一个经典的钻石继承问题,其中
A
/
B C
/
D
都是接口,我有
AImpl(A)
|
|
BImpl(B) CImpl(C)
|
|
DImpl(B,C)
| F(C)
|
E(B,C)
其中类E
实现接口B
和C
,但F
仅实现接口C
。
由于缺乏多重继承,我目前在DImpl
和CImpl
中有重复的功能。
我刚刚修复了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
的引用,然后根据您的运行时需要将BIml
或CImpl
实例传递给它。这样,您只需更改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的所有方法,并通过编写的对象委托调用。
一个常见的替代方案是使用组合而不是继承。说D
是B
,也是C
有意义吗?D
真的需要公开B
和C
中包含的每个公共方法吗?如果这些问题的答案都是否定的,那么继承就不是适合这项工作的工具。
如果您想进一步了解组合的优势,请参阅有效Java中的本章:支持组合而非继承