我有一个经典的钻石继承问题
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
,反之亦然,因为代码库不断增长,因此不可持续。是否存在将两者的共享代码放在一个地方的既定最佳实践,尽管不允许多重继承?
编辑 - 具有多重继承的解决方案应该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()
。
答案 0 :(得分:2)
在Java 8中,您可以在接口中实现默认方法,因此如果您有一个具有常见实现的接口,只需在接口内部定义它们,并在需要稍微更改它时覆盖它们。当然,假设您使用的是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");
}
}
如您所见,您可以在界面中实现默认方法,并在具体类中使用它。在这种情况下,ElectriCar是可驱动的和机器,但我们希望它使用Driveable start()方法,因为在一天结束时,无论我们的车中有多少台机器(计算机),我们仍然只想驾驶它。
这只是一个例子,虽然这个例子可能有点奇怪,但我希望它有助于理解能够实现默认方法。
您的示例来源的更新:
在你的Monster and Animal能够运行的情况下,你应该有一个RunnableCreature接口,其实现为run()
。这样,如果Monster和Animal运行相同,他们可以引用默认的run()
方法,否则它可以覆盖它并实现它自己的方法。
如果您需要默认方法来操作变量,那么具有相同run()方法的两个(或更多)类将具有公共属性,因此应具有公共基类。您可以将此基类传递给默认方法并根据需要操作其变量。
答案 1 :(得分:2)
这是一个非常糟糕的设计。你不应该首先使用钻石结构,因为它可以在界面中使用。
OOP的基本原则之一是
首选组合而不是继承!
我所说的是你根本不需要接口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
不会扩展或实现任何内容。
这就是我所说的 - 首选组合而不是继承!。
答案 2 :(得分:1)
不要在D中实现C,而不是继承在D中构建它。这样你就不会复制你的代码并只有一个C的实现。
如果你以某种方式需要从C继承(我认为你需要审查设计),那么我仍然建议你编写C并在D中实现C的所有方法并通过组合对象委托调用。
答案 3 :(得分:1)
一种常见的替代方法是使用组合而不是继承。说D
是 B
而也是 C
是否有意义? D
是否真的需要公开B
和C
中包含的每个公开方法?如果这些问题的答案都是否定的,那么继承就不适合这项工作。
如果您想进一步阅读撰写的优点,请参阅Effective Java:Favor composition over inheritance