前置知识
- 了解面向对象三大特性 浅谈 Java 三大特性的意义
- 熟悉使用接口
前言
在前文我们讲解继承的特性时提到,继承为解决代码复用问题而生,但是在实际使用时,需要多用组合,少用继承。
有同学可能会问,组合是什么?
事实上,组合可以泛指一个类作为另一个类的成员变量出现。如下方代码:
//聚合,不依赖的包含关系。即使对象KUNKUN消亡了,这种关系下的Chick对象也不会消亡 class KUNKUN { private Chick chick; public KUNKUN(Chick chick){ this.chick = chick; } } //组合,依赖的包含关系。对象KUNKUN消亡了,这种关系下的Chick对象会随之消亡 class KUNKUN { private Chick chick; public KUNKUN(){ this.chick = new Chick(); } } 复制代码
我们在日常识别或者使用中,可以把以上的 聚合 ,组合 这两种关系都称之为 组合关系
理清以上关系,我们会发现,事实上我们使用组合的频率是很高的。那组合的作用是什么呢,组合的作用和继承实际上是一样的,都是代码复用,所以这也引出了我们的问题,组合和继承这两者,我们该如何选择呢?
正如上文所说,答案是:多用组合,少用继承。这是业界大牛们经过理论推敲和实践得出的答案
本文带大家来探讨一下,这个业界标准的原因,以及回答什么时候用继承?什么时候用组合?
少用继承的原因
首先回答为何少用继承。
继承的主要功能是代码复用。但是我们是不是一旦需要复用就使用继承就好了呢?
事实上,继承在除了能解决代码复用,它还表示着类与类之间的实际关系。一旦出现了继承关系,就代表了子类以及父类的关系。所以如果仅仅只是代码复用,但是类与类之间却是并没有实际意义的继承关系的,就胡乱去定义一个父类,给不相干的子类去继承,会造成逻辑的混乱。例如 NoteBook
类要复用到 Person
类的 getID
() 代码,我们使用继承就会乱了套了。所以对于继承,需要满足高层次抽象的,有父子类关系的,我们才去使用。
所以,我们使用继承的时候,要设计好继承的父类,子类的关系。父类永远是比子类更加抽象,覆盖的范围更加广泛的。
譬如我们设定鸟类为父类,各种鸟为子类。父类定义的一个基础方法是会飞,子类在继承这个鸟类的父类后,再拓展其他的不同品种鸟类的特性。
但是之后我们会发现,很多鸟类还是不会飞的。这个 会飞 作为父类特性就不合适了,这时候我们选用嘴为 喙 的为父类的特性。那会飞怎么办,会飞的特性应该在父类中去除。因为基于迪米特法则,我们定义的父类需要是最小知识范围的。意思就是类中不能有多余的东西,子类都能使用到的才是最优的定义法则。所以我们应该去掉 会飞 这个父类的方法。
但确实有大部分鸟类是会飞的,这时候我们可以分 会飞的鸟类 和 不会飞的鸟类 分别继承鸟类这一个父类,然后其他子类再分别对于的继承这两个类。可事实上,类的特性远不止这么简单,在会飞的鸟和不会飞的鸟这两个范围内又出现更多的特性怎么办,那我们就需要使用到多层继承的办法了。我们需要在会飞以及不会飞的下一层,或者同一层等,继续建立新的父类。
但随着定义的特性变得越来越多 ,会出现继承关系复杂的问题。我们的继承树会变得很庞大,父类的数量也会层层继承,出现很多次的继承关系。
这直接导致了:
- 代码可读性变差,了解一个类还需要了解清楚其各级父类
- 破坏封装特性,父类实现细节暴露给子类。父类修改代码,导致所有子类逻辑都会变更
为何多用组合
那既然组合也可以实现继承的代码复用特性,所以选用组合是可以替代继承的。
那在父类的特性众多的时候,如何不进行多重继承,但是又能达到其清晰的拥有众多特性的效果呢?
我们使用 “组合+接口+委托” 可以清晰且简单的实现,且完美解决了多重继承带来的弊端
这种方法可提高代码的可读性,也可以降低耦合。而事实上的业务情况中,我们是经常有这类需求的,这也是我们需要多用到组合的原因。
具体代码如下(From:《设计模式之美》):
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)使用了继承关系。