老生常谈,组合优于继承,多用组合少用继承
。为什么继承不建议使用了?组合解决了继承什么问题呢?组合与继承该如何作出选择?我们一起来看看这些问题。
继承犯了什么错?遭人嫌弃?
继承是面对对象的四大特性之一,表示 is-a
的类关系。继承特性解决了代码复用的问题,但是继承的深度过度,代码会变得更复杂,变得更难以维护。继承的使用,一直存在很大的争议,甚至还有人认为他是一种反模式,被建议尽量少用。
我们看看继承的问题。
假如定义一个关于虫的类。车类我们定义抽象类为 AbstractInsect
,在抽象类中定义虫固有的属性和方法。
public abstract class AbstractInsect {
//...
}
我们知道大部分的虫类都是不能飞的,但是也不排除部分的昆虫是有飞行能力,比如飞蚁、蚊子。这时候,我们怎么改造这个抽象类呢?
我们可以直接在AbstractInsect
类中,定义一个新的方法fly()
吗?
改造后的代码:
public abstract class AbstractInsect {
public void fly() {
//...
}
//...
}
// 毛毛虫
public class Caterpillar extends AbstractInsect {
public void fly() {
throw new UnSupportedMethodException("I can't fly.'");
}
//...
}
// 蚊子
public class Mosquito extends AbstractInsect {
public void fly() {
//...
}
//...
}
这种实现方式虽然可以解决问题,但是会带来诸多问题。一方面增加了上层业务的工作量;另一方面,违反了迪米特法则,暴露了有问题的接口给外部调用,容易误用。
你可以会提出疑问,可以针对 AbstractInsect
做进一步抽象吗?针对飞行能力,拆分成会飞的虫类AbstractFlyInsect
和不会飞的虫 AbstractUnflyInsect
,这样毛毛虫 Caterpillar
继承 AbstractUnflyInsect
,蚊子 Mosquito
继承AbstractFlyInsect
,问题不就解决了么?
到这里,继承关系已经变成了3层了,目前还可以接受,层次比较浅,也算是一种较良好的设计。但是随着功能的迭代,需求提出需要增加是否可以叫的能力。按照上面的方式进行扩展,我们应该对会飞的虫类AbstractFlyInsect
和不会飞的虫 AbstractUnflyInsect
俩个抽象类做进一步的抽象,分别为会飞会叫的虫类、会飞不会叫的虫类、不会飞会叫的虫类、不会飞不会叫的虫类。这时,继承关系已经变成4层了。
以此类推,随着能力的添加,抽象的组合就会成倍增加,类的继承层次也会更深更复杂。
这里存在俩个比较大的问题:
- 代码的可读性很差。若要了解清楚父类方法实现,要一直往上层追溯,直到根部方法;
- 破坏了类的封装性。不知道你有没有发现,增加能力fly时,需要修改子类的继承。一旦父类的代码修改,就会影响到子类的逻辑;
综上所述,继承层次过深、继承关系过于复杂会影响到代码的可读性和可维护性。
组合是否能解决继承的问题
其实,我们基于组合、接口、委托
来解决层次过深、继承关系过于复杂的问题。
接口表示具有某种行为特性,has-a
特性。上面的问题,针对会飞的能力定义接口FlyAble
,会叫的能力定义接口TweetAble
。
public interface Insect {
}
public interface FlyAble {
void fly();
}
public interface TweetAble {
void tweet();
}
// 毛毛虫
public class Caterpillar implements Insect, TweetAble {
public void tweet() {
//..
}
//...
}
// 蚊子
public class Mosquito implements Insect, FlyAble, TweetAble {
public void fly() {
//...
}
public void tweet() {
//...
}
//...
}
上面的代码很好地解决了问题。但是这里又出现了另外的问题,毛毛虫Caterpillar
和蚊子Mosquito
都实现了TweetAble
接口,这里有可能会出现重复的逻辑实现,那么这个问题应该如何解决呢?
我们可以通过委托的方式去解决问题。
public class FlyAbility implements FlyAble {
public void fly() {
//..
}
}
public class TweetAbility implements TweetAble {
public void tweet() {
//..
}
}
// 毛毛虫
public class Caterpillar implements Insect, TweetAble {
private TweetAbility tweetAbility = new TweetAbility();
public void tweet() {
tweetAbility.tweet();
}
//...
}
// 蚊子
public class Mosquito implements Insect, FlyAble, TweetAble {
private FlyAbility flyAbility = new FlyAbility();
private TweetAbility tweetAbility = new TweetAbility();
public void fly() {
flyAbility.fly();
}
public void tweet() {
tweetAbility.tweet();
}
//...
}
继承,表示 is-a
的类关系,支持多态,解决代码复用。is-a
的类关系可以通过组合和接口的 has-a
的类关系替代;多态可以用接口代替;代码复用可以用组合和委托来实现。因此,我们完全可以使用组合、接口、委托的手段,来完全替代继承,特别是一些继承关系复杂的代码。
组合与继承该如何作出选择?
如果类之间的继承结构稳定(不会轻易改变,相对稳定),继承层次比较浅(超过2层就会变得复杂了),继承关系不复杂,我们就可以大胆地使用继承。反之,尽量使用组合、接口、委托来替代继承。
除此之外,还有一些设计模式会固定使用继承或者组合。比如,装饰者模式(decorator pattern)、策略模式(strategy pattern)、组合模式(composite pattern)等都使用了组合关系,而模板模式(template pattern)使用了继承关系。
尽管有些人喊着“多用组合少用继承”口号,甚至“杜绝继承”。但是在我看来,继承并非一无是处,只要他的缺点能有效控制,他依然可以发挥它的优势。在适当的场合,合理地选择继承还是组合,要因地制宜。