继承是代码复用的最佳方案吗?

简介: 继承,一个父类可有许多个子类。父类就是把一些公共代码放进去,之后在实现其他子类时,少写一些代码。代码复用,很多人觉得继承就是绝佳方案。若把继承理解成代码复用,更多是站在子类角度向上看。在客户端代码使用时,面对的是子类,这种继承叫实现继承

继承,一个父类可有许多个子类。父类就是把一些公共代码放进去,之后在实现其他子类时,少写一些代码。


代码复用,很多人觉得继承就是绝佳方案。若把继承理解成代码复用,更多是站在子类角度向上看。在客户端代码使用时,面对的是子类,这种继承叫实现继承:


Child object = new Child();

还有一种看待继承的角度:从父类往下看,客户端使用时,面对的是父类,这种继承叫接口继承:


Parent object = new Child();


但接口继承更多和多态相关。本文主要讨论实现继承。


不推荐实现继承:


继承很宝贵,Java只支持单继承

一个类只能有一个父类,一旦继承的位置被实现继承占据,再想做接口继承就难了

实现继承通常也是一种受程序设计语言局限的思维方式

很多语言,不使用继承,也有代码复用方案

1 案例

产品报表服务,其中的某服务:查询产品信息。该查询过程通用,别的服务也可用。所以,我把它放父类以复用:


class BaseService {

 // 获取相应产品信息

 protected List<Product> getProducts(List<String> product) {

   ...

 }

}


// 生成报表服务

class ReportService extends BaseService {

 public void report() {

   List<Product> product = getProduct(...);

   // 生成报表

   ...

 }

}


ReportService没有继承任何类,但也可复用代码,即ProductFetcher模块。

这样,若我需要有个获取产品信息的地方,它不必非得是个服务,我无需继承任何类。


获取产品信息、生成报表是两件事,只是因为在生成报表过程,需要获取产品信息,所以,它有个基类。


不用继承的实现:


class ProductFetcher {

 // 获取相应的产品信息

 public List<Product> getProducts(List<String> product) {

   ...

 }

}


// 生成报表服务

class ReportService {

 private ProductFetcher fetcher;

 

 public void report() {

   List<Product> product = fetcher.getProducts(...);

   // 生成报表

   ...

 }

}



这就是组合:ReportService里组合一个ProductFetcher。

设计通用原则:组合优于继承。即若一个方案既能用组合实现,也能用继承实现,那就用组合。


所以,要写继承以实现代码复用时,问问自己,这是接口继承,还是实现继承?

若是实现继承,是不是可以写成组合?


2 面向组合编程

可以组合的根因:获取产品信息、生成报表服务本是两件事(分离关注点)。

你要是看出是两件事了,就不会把它们放一起。


分解是设计的第一步,分解粒度越小越好。当可分解出多个关注点,每个关注点就是个独立类。最终类由这一个个小类组合而得,即面向组合编程。按面向组合思维:为增加复杂度,增加一个报表生成器(ReportGenerator),在获取产品信息后,生成报表:


class ReportService {

 private ProductFetcher fetcher;

 

 private ReportGenerator generator;

 

 public void report() {

   List<Product> product = fetcher.getProducts(...);

   // 生成报表

   generator.generate(product);

 }

}


OOP面向的是“对象”,不是类!很多程序员习惯把对象理解成类的附属品,但在Alan Kay的理解中,对象本身就是独立个体。所以,有些语言支持直接在对象操作。


现在,想给报表服务新增接口:处理产品信息。这样的处理只会影响这里的一个对象,而同样是这个ReportService的其他实例,则完全不受影响。


好处

不必写那么多类,根据需要,在程序运行时组合出不同对象。

Java只有类这种组织方式,所以,很多有差异的概念只能用类这一个概念表示,思维受到限制,不同语言则提供不同的表现形式,让概念更加清晰。


前面只是面向组合编程在思考方式的转变,现在看设计差异。


3 案例


字体类(Font)需求:支持加粗、下划线、斜体(Italic),且能任意组合。


3.1 继承

需8个类:

9.png


3.2 组合

字体类(Font)只需三个独立维度:是否加粗、下划线、斜体。

若再来一种需求,变成4种,采用继承,类数量膨胀到16个,而组合只需再增加一个维度。把一个M*N问题,设计转成M+N。


Java在面向组合编程方面能力较弱,但Java在尝试不同方案。早期尝试有Qi4j,后来Java 8加入default method,在一定程度上也可支持面向组合编程。


4 DCI

继承是OOP原则之一,但编码实践中能用组合尽量使用组合。

DCI也是一种编码规范,对OOP的一种补充,核心思想也是关注点分离。


DCI是对象的Data数据, 对象使用的Context场景, 对象的Interaction交互行为三者简称, 是一种特别关注行为的模式(可对应GoF行为模式),而MVC模式是一种结构性模式,DCI可使用演员场景表演来解释,某实体在某场景中扮演包公,实施包公升堂行为;典型事例是银行帐户转帐,转帐这行为按DDD很难划分到帐号对象,它是跨两个帐号实例之间的行为,可看成是帐号这个实体(PPT,见四色原型)在转帐这个场景,实施了钞票划转行为,这种新角度更贴近需求和自然,结合四色原型 DDD和DCI可以一步到位将需求更快地分解落实为可运行的代码,是国际上软件领域的一场革命。 摘自 https://www.jdon.com/dci.html


5 总结

组合优于继承。 复用方式背后的编程思想:面向组合编程。它给我们提供了一个不同的视角,但支撑面向组合编程的是分离关注点。将不同关注点分离,每个关注点成为一个模块,在需要时组装。面向组合编程,在设计本身上有很多优秀地方,可降低程序复杂度,更是思维转变。


参考


https://www.infoq.cn/article/2007/11/qi4j-intro

https://en.wikipedia.org/wiki/Data,_context_and_interaction

目录
相关文章
|
3月前
|
设计模式 Java
装饰者模式:打破继承限制,实现灵活的功能扩展
装饰者模式:打破继承限制,实现灵活的功能扩展
29 0
|
8月前
继承(6种方式)以及优缺点
继承(6种方式)以及优缺点
|
2月前
|
算法 编译器 C语言
【C/C++ 编程题 01】用C++设计一个不能被继承的类
【C/C++ 编程题 01】用C++设计一个不能被继承的类
24 0
|
2月前
|
设计模式
代码复用
代码复用
15 3
|
3月前
|
前端开发 JavaScript API
|
5月前
|
编译器 C++
[C++] 面向对象的三大特性:封装、继承和多态
[C++] 面向对象的三大特性:封装、继承和多态
34 0
|
6月前
|
Java
Java接口:实现多重继承,促进代码复用与扩展的强大工具
Java接口:实现多重继承,促进代码复用与扩展的强大工具
|
8月前
|
设计模式 数据安全/隐私保护
面向对象编程基础:封装、继承、多态与抽象的全面解析
面向对象编程基础:封装、继承、多态与抽象的全面解析
30 0
|
9月前
|
Java C++
面对对象三大特性:封装、继承、多态
面对对象三大特性:封装、继承、多态
|
10月前
浅谈 面向对象三大特性:封装 继承 多态
浅谈 面向对象三大特性:封装 继承 多态
70 0