本章用来讲述设计模式中的另一个原则——组合优于继承原则,又称为合成复用原则。

一、基本介绍

该原则是尽量使用合成/聚合的方式,而不是使用继承。

上面一句话就是对该原则的核心,但单单看这么一句话还是显得过于枯燥与不知所云,下面我们还是结合具体的场景进行代码推进,对该原则进行讲解。

二、场景

假设我们需要设计这样一个集合,每次向里面加入元素时,count都加一。例如:

最初集合是空集合,此时我们向里面加入"a",此时集合为{"a"},那么此时count = 1;当加入"b","c"两个元素时,集合为{"a","b","c"},此时count=3;此时再删除"a","c"两个元素,集合为{"b"},count仍然等于3;最后再加入"d",集合为{"b","d"}count=4

所以该场景是,不论中间是否删除元素,我们只统计加入到集合中的元素的次数,进行返回。

三、代码演进

根据上面的场景,我们开始一步步编写代码。

3.1 代码一
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MySet01<E> extends  HashSet<E>{

private int count = 0;

@Override
public boolean add(E e) {
++count;
return super.add(e);
}

@Override
public boolean addAll(Collection<? extends E> c) {
boolean modified = false;
for (E e : c)
if (add(e))
modified = true;
return modified;
}

public int getCount() {
return count;
}
}

此时,HashSet源码中,所有add的入口就是该方法add方法。

但是如果这么设计,出现的问题有:

  1. 如果在新的jdk版本中,HahsSet突然多了一个元素加入集合的入口方法,例如addSome,这个方法是我们不可预知的。我们的MySet01根本没有重写新版本中出现的addSome方法。这样,在新版本中,我们的MySet也继承了addSome方法,当使用addSome方法添加元素时,根本不会去统计元素的数量。
  2. 我们重写了addAll方法,和add方法。在HashSet的所有方法中,难免有一些其他方法,会依赖addAll方法和add方法。如果我们就这样随便重写了HashSet类中的某些方法,就会导致其他依赖于这些方法的方法,容易出现问题,不好排查。

那么为了避免以上的问题,肯定就会有人想到那我们自己定义两个方法,不重写HashSet中的这两个方法不就好了,说得很对,我们继续改写来看一看。

3.2 代码二
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MySet02<E> extends HashSet<E> {
private int count;

public boolean add2(E e) {
++count;
return super.add(e);
}

public boolean addAll2(Collection<? extends E> c) {
boolean modified = false;
for (E e : c)
if (add(e))
modified = true;
return modified;
}

public int getCount() {
return count;
}
}

从上面的代码中,我们自己定义了两个方法—— add2和addAll2。此时这段代码看起来是解决了我们上面一段代码所遗留的问题,但是该问题又产生了两个新问题。

首先,如果用户需要用到该计数功能时,我们需要提供一份文档给用户,告诉他你需要调用add2和addAll2才能使用该功能,这种方式未免对用户太过于苛刻了。

以上这个问题还不是最致命的。现在就将产生的问题做一个总结:

  1. 目前这种情况对用户要求过于苛刻,用户必须看类的API文档,看完了还要使用add2和addAll2这两个方法,不能出错。
  2. 更加致命的是,如果在未来的jdk版本中,HashSet恰恰多两个API,叫add2和addAll2,那么就又出现了代码一种的两个问题了。

因此,继承应该就已经走到绝境了。这个时候,我们就要考虑到组合大于继承的原则了。

3.3 代码三

针对代码二出现的问题,先做出如下改进:

  1. 我们的MySet,再也不要去继承HashSet了。
  2. 取而代之,我们让MySet和HashSet发生关联关系(组合)。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MySet03<E> {

private int count = 0;
private Set<E> set = new HashSet<>();

public boolean add(E e) {
++count;
return set.add(e);
}

public boolean addAll(Collection<? extends E> e) {

count += e.size();
return set.addAll(e);
}

public int getCount() {
return count;
}
}

该段代码中,对于用户来说,完全隐藏了add和addAll细节,用户只管调用它们即可。哪怕在未来的JDK中,HashSet又增加了其他的add方法,但是用户只能调用我提供这两个add方法。所以,是不是此时组合的优势就体现出来了。

四、总结

通过代码演进这一节,我们看出了组合的优势。这时有人肯定会产生怀疑,组合既然这么好,我们干嘛还要继承呢?

难道以后都不能使用继承了吗?

难道以后都不能进行方法重写了吗?

非也!请看以下的情况。

如果父类作者,和子类的作者,不是同一个人。那么就不要使用继承,因为你不知道父类作者以后会对代码改动时,做出啥事,你两又沟通不上。因为,父类作者不知道,未来的子类,会重写自己的哪个方法;那么子类作者不知道,未来的父类,会加入什么新方法(和自己的产生冲突)。

如果父类作者和子类作者是同一个人,那么就可以随意使用继承了。因为,自己当然知道,每个方法都是什么作用,作者可以很好的控制父类和子类。

我们自己写代码,继承,重写,随便使用;如果我们仅仅是为了复用代码,而继承别人的类,难免会出现“沟通”上的问题,所以谨慎使用继承。


Comment