本章用来讲述设计模式中的另一个原则——组合优于继承原则,又称为合成复用原则。
一、基本介绍
该原则是尽量使用合成/聚合的方式,而不是使用继承。
上面一句话就是对该原则的核心,但单单看这么一句话还是显得过于枯燥与不知所云,下面我们还是结合具体的场景进行代码推进,对该原则进行讲解。
二、场景
假设我们需要设计这样一个集合,每次向里面加入元素时,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 | public class MySet01<E> extends HashSet<E>{ |
此时,HashSet
源码中,所有add
的入口就是该方法add
方法。
但是如果这么设计,出现的问题有:
- 如果在新的jdk版本中,
HahsSet
突然多了一个元素加入集合的入口方法,例如addSome
,这个方法是我们不可预知的。我们的MySet01
根本没有重写新版本中出现的addSome
方法。这样,在新版本中,我们的MySet也继承了addSome方法,当使用addSome
方法添加元素时,根本不会去统计元素的数量。 - 我们重写了addAll方法,和add方法。在
HashSet
的所有方法中,难免有一些其他方法,会依赖addAll
方法和add
方法。如果我们就这样随便重写了HashSet
类中的某些方法,就会导致其他依赖于这些方法的方法,容易出现问题,不好排查。
那么为了避免以上的问题,肯定就会有人想到那我们自己定义两个方法,不重写HashSet
中的这两个方法不就好了,说得很对,我们继续改写来看一看。
3.2 代码二
1 | public class MySet02<E> extends HashSet<E> { |
从上面的代码中,我们自己定义了两个方法—— add2和addAll2。此时这段代码看起来是解决了我们上面一段代码所遗留的问题,但是该问题又产生了两个新问题。
首先,如果用户需要用到该计数功能时,我们需要提供一份文档给用户,告诉他你需要调用add2和addAll2才能使用该功能,这种方式未免对用户太过于苛刻了。
以上这个问题还不是最致命的。现在就将产生的问题做一个总结:
- 目前这种情况对用户要求过于苛刻,用户必须看类的API文档,看完了还要使用add2和addAll2这两个方法,不能出错。
- 更加致命的是,如果在未来的jdk版本中,HashSet恰恰多两个API,叫add2和addAll2,那么就又出现了代码一种的两个问题了。
因此,继承应该就已经走到绝境了。这个时候,我们就要考虑到组合大于继承的原则了。
3.3 代码三
针对代码二出现的问题,先做出如下改进:
- 我们的MySet,再也不要去继承HashSet了。
- 取而代之,我们让MySet和HashSet发生关联关系(组合)。
代码如下:
1 | public class MySet03<E> { |
该段代码中,对于用户来说,完全隐藏了add和addAll细节,用户只管调用它们即可。哪怕在未来的JDK中,HashSet
又增加了其他的add方法,但是用户只能调用我提供这两个add方法。所以,是不是此时组合的优势就体现出来了。
四、总结
通过代码演进这一节,我们看出了组合的优势。这时有人肯定会产生怀疑,组合既然这么好,我们干嘛还要继承呢?
难道以后都不能使用继承了吗?
难道以后都不能进行方法重写了吗?
非也!请看以下的情况。
如果父类作者,和子类的作者,不是同一个人。那么就不要使用继承,因为你不知道父类作者以后会对代码改动时,做出啥事,你两又沟通不上。因为,父类作者不知道,未来的子类,会重写自己的哪个方法;那么子类作者不知道,未来的父类,会加入什么新方法(和自己的产生冲突)。
如果父类作者和子类作者是同一个人,那么就可以随意使用继承了。因为,自己当然知道,每个方法都是什么作用,作者可以很好的控制父类和子类。
我们自己写代码,继承,重写,随便使用;如果我们仅仅是为了复用代码,而继承别人的类,难免会出现“沟通”上的问题,所以谨慎使用继承。