多用组合少用继承

为什么不推荐使用继承

继承是面向对象的四大特性之一,用来表示类之间的 is-a 关系,可以解决代码复用的问题。虽然继承有诸多作用,但继承层次过深、过复杂,也会影响到代码的可维护性

假设我们要设计一个关于鸟的类。我们将“鸟类”这样一个抽象的事物概念,定义为一个抽象类 AbstractBird。所有更细分的鸟,比如麻雀、鸽子、乌鸦等,都继承这个抽象类。

我们知道,大部分鸟都会飞,那我们可不可以在 AbstractBird 抽象类中,定义一个 fly() 方法呢?答案是否定的。尽管大部分鸟都会飞,但也有特例,比如鸵鸟就不会飞。鸵鸟继承具有 fly() 方法的父类,那鸵鸟就具有“飞”这样的行为,这显然不符合我们对现实世界中事物的认识。当然,你可能会说,我在鸵鸟这个子类中重写(override)fly() 方法,让它抛出 UnSupportedMethodException 异常不就可以了吗?具体的代码实现如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class AbstractBird 
{
//...省略其他属性和方法...
public void fly()
{
//...
}
}

public class Ostrich extends AbstractBird
{
// 鸵鸟
//...省略其他属性和方法...
public void fly()
{
throw new UnSupportedMethodException("I can't fly.'");
}
}

这种设计思路虽然可以解决问题,但不够优美。因为除了鸵鸟之外,不会飞的鸟还有很多,比如企鹅。对于这些不会飞的鸟来说,我们都需要重写 fly() 方法,抛出异常。这样的设计,一方面,徒增了编码的工作量;另一方面,也违背了我们之后要讲的最小知识原则(Least Knowledge Principle,也叫迪米特法则),暴露不该暴露的接口给外部,增加了类使用过程中被误用的概率。

组合的优势

实际上,我们可以利用组合(composition)接口(interface)委托(delegation)三个技术手段,一块儿来解决刚刚继承存在的问题。

我们前面讲到接口的时候说过,接口表示具有某种行为特性。针对“会飞”这样一个行为特性,我们可以定义一个 Flyable 接口,只让会飞的鸟去实现这个接口。对于会叫、会下蛋这些行为特性,我们可以类似地定义 Tweetable 接口、EggLayable 接口。我们将这个设计思路翻译成 Java 代码的话,就是下面这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public interface Flyable 
{
void fly();
}
public interface Tweetable
{
void tweet();
}
public interface EggLayable
{
void layEgg();
}
public class Ostrich implements Tweetable, EggLayable
{
// 鸵鸟
//...省略其他属性和方法...
@Override
public void tweet()
{
//...
}
@Override
public void layEgg()
{
//...
}
}
public class Sparrow implements Flyable, Tweetable, EggLayable
{
// 麻雀
//...省略其他属性和方法...
@Override
public void fly()
{
//...
}
@Override
public void tweet()
{
//...
}
@Override
public void layEgg()
{
//...
}
}

不过,我们知道,接口只声明方法,不定义实现。也就是说,每个会下蛋的鸟都要实现一遍 layEgg() 方法,并且实现逻辑是一样的,这就会导致代码重复的问题。那这个问题又该如何解决呢?

我们可以针对三个接口再定义三个实现类,它们分别是:实现了 fly() 方法的 FlyAbility 类、实现了 tweet() 方法的 TweetAbility 类、实现了 layEgg() 方法的 EggLayAbility 类。然后,通过组合和委托来消除代码重复。具体的代码实现如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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(); // 委托
}
}

我们知道继承主要有三个作用:表示 is-a 关系,支持多态特性,代码复用。而这三个作用都可以通过其他技术手段来达成:

  • is-a 关系,我们可以通过组合和接口的 has-a 关系来替代;
  • 多态特性我们可以利用接口来实现;
  • 代码复用我们可以通过组合和委托来实现;

组合 or 继承

如果类之间的继承结构稳定(不会轻易改变),继承层次比较浅(比如,最多有两层继承关系),继承关系不复杂,我们就可以大胆地使用继承。反之,系统越不稳定,继承层次很深,继承关系复杂,我们就尽量使用组合来替代继承

除此之外,还有一些设计模式会固定使用继承或者组合。比如,装饰者模式(decorator pattern)、策略模式(strategy pattern)、组合模式(composite pattern)等都使用了组合关系,而模板模式(template pattern)使用了继承关系。

组合并不完美,继承也不是一无是处。