九、设计模式之美-09| 理论七:为何说要多用组合少用继承?如何决定该用组合还是继承?
在面向对象编程中,有一条非常经典的设计原则,那就是:组合优于继承,多用组合少用继承。为什么不推荐使用继承?组合相比继承有哪些优势?如何判断该用组合还是继承?今天,我们就围绕着这三个问题,来详细讲解一下这条设计原则。
一.为什么不推荐使用继承?
继承是面向对象的四大特性之一,用来表示类之间的 is-a 关系,可以解决代码复用的问题。虽然继承有诸多作用,但继承层次过深、过复杂,也会影响到代码的可维护性。
1、举例说明
现在要设计一个“鸟类”的抽象事物概念,定义一个抽象类:AbstractBird,所有更细分的鸟,比如麻雀、鸽子、乌鸦等,都继承这个抽象类。
是否可以在这个抽象类中定义一个fly方法?答案是不可以的,因为虽然绝大多数鸟都会飞,但是还是存在鸵鸟,企鹅这种不会飞的鸟类,如果鸵鸟继承了有fly方法的弗雷,那么鸵鸟就具有了飞的能力,不符合现实事务。虽然可以通过重写fly方法,让鸵鸟类抛出UnSupportedMethodException 异常,但是除了体哦鸟还有其他不会飞的鸟,如果每个不会飞的鸟都要抛出一次异常,一方面增加了编码的工作量,另一方面也违背了最小知识原则,暴露不该暴露的接口给外部,增加了类使用过程中被错误使用的概率。
public class AbstractBird {
//...省略其他属性和方法...
public void fly() { //... }
}
public class Ostrich extends AbstractBird { //鸵鸟
//...省略其他属性和方法...
public void fly() {
throw new UnSupportedMethodException("I can't fly.'");
}
}
如果把AbstractBird 类派生出两个更加细分的抽象类:会飞的鸟类 AbstractFlyableBird 和不会飞的鸟类 AbstractUnFlyableBird,让麻雀、乌鸦这些会飞的鸟都继承 AbstractFlyableBird,让鸵鸟、企鹅这些不会飞的鸟,都继承 AbstractUnFlyableBird 类,这样继承关系就变成了三层,如果继承关系比较简单,这样也是一种可以接受的设计思路,但是如果我们再关注鸟会被不会叫,鸟吃什么食物的时候,鸟会下什么样的蛋,考虑的行为因素越多,组合种类就越多,类的继承层次会越来越深,继承关系会越来越复杂,一方面,会导致代码的可读性变差。因为我们要搞清楚某个类具有哪些方法、属性,必须阅读父类的代码、父类的父类的代码……一直追溯到最顶层父类的代码。另一方面,这也破坏了类的封装特性,将父类的实现细节暴露给了子类。子类的实现依赖父类的实现,两者高度耦合,一旦父类代码修改,就会影响所有子类的逻辑。
二.组合相比继承有哪些优势?
我们可以利用组合(composition)、接口、委托(delegation)三个技术手段,一块儿来解决刚刚继承存在的问题。
比如这样
public interface Flyable {
void fly();
}
public class FlyAbility implements Flyable {
@Override
public void fly() { //... }
}
//省略Tweetable/TweetAbility/EggLayable/EggLayAbility
public class Ostrich implements Tweetable, EggLayable {//鸵鸟
private TweetAbility tweetAbility = new TweetAbility(); //组合
private EggLayAbility eggLayAbility = new EggLayAbility(); //组合
//... 省略其他属性和方法...
@Override
public void tweet() {
tweetAbility.tweet(); // 委托
}
@Override
public void layEgg() {
eggLayAbility.layEgg(); // 委托
}
}
三.如何判断该用组合还是继承?
尽管我们鼓励多用组合少用继承,但组合也并不是完美的,继承也并非一无是处。从上面的例子来看,继承改写成组合意味着要做更细粒度的类的拆分。这也就意味着,我们要定义更多的类和接口。类和接口的增多也就或多或少地增加代码的复杂程度和维护成本。所以,在实际的项目开发中,我们还是要根据具体的情况,来具体选择该用继承还是组合。
如果类之间的继承结构稳定(不会轻易改变),继承层次比较浅(比如,最多有两层继承关系),继承关系不复杂,我们就可以大胆地使用继承。反之,系统越不稳定,继承层次很深,继承关系复杂,我们就尽量使用组合来替代继承。
除此之外,还有一些设计模式会固定使用继承或者组合。比如,装饰者模式(decorator pattern)、策略模式(strategy pattern)、组合模式(composite pattern)等都使用了组合关系,而模板模式(template pattern)使用了继承关系。