继承和组合如何选择|设计模式基础

简介: 在前文我们讲解继承的特性时提到,继承为解决代码复用问题而生,但是在实际使用时,需要多用组合,少用继承。

前置知识

前言

在前文我们讲解继承的特性时提到,继承为解决代码复用问题而生,但是在实际使用时,需要多用组合,少用继承

有同学可能会问,组合是什么?

事实上,组合可以泛指一个类作为另一个类的成员变量出现。如下方代码:

//聚合,不依赖的包含关系。即使对象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() 代码,我们使用继承就会乱了套了。所以对于继承,需要满足高层次抽象的,有父子类关系的,我们才去使用

所以,我们使用继承的时候,要设计好继承的父类,子类的关系。父类永远是比子类更加抽象,覆盖的范围更加广泛的

譬如我们设定鸟类为父类,各种鸟为子类。父类定义的一个基础方法是会飞,子类在继承这个鸟类的父类后,再拓展其他的不同品种鸟类的特性。

但是之后我们会发现,很多鸟类还是不会飞的。这个 会飞 作为父类特性就不合适了,这时候我们选用嘴为 的为父类的特性。那会飞怎么办,会飞的特性应该在父类中去除。因为基于迪米特法则,我们定义的父类需要是最小知识范围的。意思就是类中不能有多余的东西,子类都能使用到的才是最优的定义法则。所以我们应该去掉 会飞 这个父类的方法。

但确实有大部分鸟类是会飞的,这时候我们可以分 会飞的鸟类不会飞的鸟类 分别继承鸟类这一个父类,然后其他子类再分别对于的继承这两个类。可事实上,类的特性远不止这么简单,在会飞的鸟和不会飞的鸟这两个范围内又出现更多的特性怎么办,那我们就需要使用到多层继承的办法了。我们需要在会飞以及不会飞的下一层,或者同一层等,继续建立新的父类。

但随着定义的特性变得越来越多 ,会出现继承关系复杂的问题。我们的继承树会变得很庞大,父类的数量也会层层继承,出现很多次的继承关系。

这直接导致了:

  1. 代码可读性变差,了解一个类还需要了解清楚其各级父类
  2. 破坏封装特性,父类实现细节暴露给子类。父类修改代码,导致所有子类逻辑都会变更

为何多用组合

那既然组合也可以实现继承的代码复用特性,所以选用组合是可以替代继承的。

那在父类的特性众多的时候,如何不进行多重继承,但是又能达到其清晰的拥有众多特性的效果呢?

我们使用 “组合+接口+委托” 可以清晰且简单的实现,且完美解决了多重继承带来的弊端

这种方法可提高代码的可读性,也可以降低耦合。而事实上的业务情况中,我们是经常有这类需求的,这也是我们需要多用到组合的原因。

具体代码如下(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)使用了继承关系。



相关文章
|
6月前
|
设计模式
二十三种设计模式全面解析-装饰器模式-超越继承的灵活装扮
二十三种设计模式全面解析-装饰器模式-超越继承的灵活装扮
|
6月前
|
设计模式
二十三种设计模式全面解析-组合模式与迭代器模式的结合应用:构建灵活可扩展的对象结构
二十三种设计模式全面解析-组合模式与迭代器模式的结合应用:构建灵活可扩展的对象结构
118 0
|
6月前
|
设计模式 存储 SQL
第四篇 行为型设计模式 - 灵活定义对象间交互
第四篇 行为型设计模式 - 灵活定义对象间交互
129 0
|
6月前
|
设计模式
二十三种设计模式全面解析-组合模式与装饰器模式的结合:实现动态功能扩展
二十三种设计模式全面解析-组合模式与装饰器模式的结合:实现动态功能扩展
|
设计模式 存储
组合设计模式解读
组合设计模式解读
|
设计模式 算法 API
常用设计模式的功能、关联和区别
常用设计模式的功能、关联和区别
|
设计模式 缓存 Java
使用组合的设计模式 | 追女孩要用的远程代理模式
上篇讲了一个使用组合的设计模式:装饰者模式。它通过继承复用了类型,通过组合复用了行为,最终达到扩展类功能的目的。 代理模式也运用了组合的实现方法,它和装饰者模式非常像,比较它们之间微妙的差别很有意思。
99 0
|
设计模式 存储 容器
设计模式之组合
设计模式之组合
132 0
设计模式之组合
|
设计模式
【设计模式】软件设计七大原则 ( 里氏替换原则 | 定义 | 定义扩展 | 引申 | 意义 | 优点 )
【设计模式】软件设计七大原则 ( 里氏替换原则 | 定义 | 定义扩展 | 引申 | 意义 | 优点 )
210 0
|
设计模式
【设计模式】抽象工厂模式 ( 简介 | 适用场景 | 优缺点 | 产品等级结构和产品族 | 代码示例 )(二)
【设计模式】抽象工厂模式 ( 简介 | 适用场景 | 优缺点 | 产品等级结构和产品族 | 代码示例 )(二)
202 0
【设计模式】抽象工厂模式 ( 简介 | 适用场景 | 优缺点 | 产品等级结构和产品族 | 代码示例 )(二)
下一篇
无影云桌面