重构分析21: 被拒绝的遗赠(Refused Bequest)
子类和父类的关系开始很简单,但是随着时间的推移有可能会变的越来越复杂。一个子类通常需要紧密的依赖其父类,但是有时会矫枉过正。
这就是继承的两面性,下面我们看看继承可能代码的Code Smell。
01 场景复现
需求描述
这是关于活动(Activity)和票(Ticket)的业务需求: 活动的主题(ActityType): session | workshop | read | TDD 活动(Activity)包含属性:日期、主题、基础价格 票有两种:普通票(Ticket)、VIP票(VIPTicket) 普通票(Ticket)的业务描述: (1)是否有Session活动:如果主题是session且活动日期是工作日则返回true,否则返回false。 (2)获得票价:如果是如果周一到周四票价=原价,如果周五返回则票价=原价x2 (3)退款:活动开始前可以进行退款。 VIP票(VIPTicket)的业务需求: (1)是否有Session活动:如果主题是session则返回true,否则返回false。 (2)获得票价:票价 = 如果是如果周一到周四票价=原价+100,如果周五返回则票价=原价x2+100 (3)是否有附加活动:如果活动主题为TDD或者制定了附加活动则返回true,否则返回false。
基于上面的业务需求,下面是一段具有“被拒绝的遗赠”Smell的代码,如下:
Activity.java
[@Getter](https://my.oschina.net/u/3288663) public class Activity { private final ActivityType type; private final LocalDate date; private final int price; public Activity(ActivityType type, LocalDate date, int price) { this.type = type; this.date = date; this.price = price; } public enum ActivityType {WORKSHOP, TDD, SESSION} }
Ticket.java
package com.page.refactoring; import java.time.DayOfWeek; public class Ticket { private final Activity activity; public Ticket(Activity activity) { this.activity = activity; } public boolean isSession() { return Activity.ActivityType.SESSION.equals(activity.getType()) && isWorkday(); } private boolean isWorkday() { return !activity.getDate().getDayOfWeek().equals(DayOfWeek.SATURDAY) && !activity.getDate().getDayOfWeek().equals(DayOfWeek.SUNDAY); } public int getPrice() { return DayOfWeek.FRIDAY.equals(activity.getDate().getDayOfWeek()) ? activity.getPrice() * 2 : activity.getPrice(); } public int refund() { return getPrice(); } }
VIPTicket.java
public class VIPTicket extends Ticket { private final boolean supportExtensionalActivities; public VIPTicket(Activity activity, boolean supportExtensionalActivities) { super(activity); this.supportExtensionalActivities = supportExtensionalActivities; } public boolean isSession() { return Activity.ActivityType.SESSION.equals(activity.getType()); } public int getPrice() { return super.getPrice() + 100; } public boolean hasExtensionalActivities() { return Activity.ActivityType.TDD.equals(activity.getType()) || supportExtensionalActivities; } }
“被拒绝的遗赠”Code Smell代码地址: https://gitlab.com/tengbai/refactoring/tree/21-refused-bequest
02 上面代码中的问题
上面的代码中Ticket和VIPTicket使用了继承。首先继承是一种有价值的机制,将公共的数据和行为放置在父类中,每个子类根据需要覆写部分特性。大部分时候能达到期望的效果,不会带来问题。但是上面的代码在使用继承时存在如下几个问题
VIPTicket继承了Ticket,虽然VIPTicket复用了Ticket的属性和部分方法,但是却使代码出现了下面的问题:
getPrice()方法不但覆写父类的方法并且并且还还调用了父类的getPrice()方法。虽然当前的结果复用的getPrice()方法没有什么问题,但是当当Ticket类上getPrice()的内部逻辑变化时会影响到VIPTicket子类。
VIPTicket提供了hasExtensionalActivities()方法,但是父类并没有该方法
Ticket提供了refund()退款功能,而VIPTicket业务中并不需要该功能,但是由于VIPTicket继承了Ticket,所以也拥有了refund()方法。这使得代码并没有按照本意来揭示业务意图。
很显然违反了LSP(里氏替换原则)。在我们经常使用的SOLID的原则中,LSP(里氏替换原则):子类必须能够替换掉他们的父类。即父类出现的地方就可以使用子类来代替,而且不会出现任何错误或者异常。
除了上面代码,继承还经常出现的问题有:
- 一个子类继承了父类但是子类中的某个方法抛出了异常,而父类中该方法并没有抛出异常。
- 一个子类继承了父类,但是子类修改了某个方法的内部行为。
- 调用者只能通过子类而不能通过父类来访问类。
- 无意义的继承,子类并不是父类的一个实例。
03 对“被拒绝的遗赠”可采取的措施和收益
首先重构上面这段代码的目的是:1,代码能够揭示业务意图;2,改善可测试性(同样的方法无需担心上下文的不同)。
1. 重新整理继承关系。
如下图,创建一个父类BasicTicket,它提供了公共的属性和方法,Ticket和VIPTicket成为兄弟子类,他们提供各自需要的方法。
重构后的代码: https://gitlab.com/tengbai/refactoring/tree/21-refused-bequest-rebuild-mapping
2. 组合优于继承
在很多次的讨论中,都会提到使用接口组合来代替继承。下面的图显示使用接口组合来解决上面的遇到的“被拒绝的遗赠”的问题。
重构后的代码: https://gitlab.com/tengbai/refactoring/tree/21-refused-bequest-refactoring-with-interface
3. 使用代理取代继承
将不同的变化原因委托给不同的类。委托是类之间的常规关系,使用委托接口更加清晰,耦合度更低。
上面的例子中使用委托来代替继承是最简单的一个修改方式。如下图:
重构后的代码: https://gitlab.com/tengbai/refactoring/tree/21-refused-bequest-refactoring-with-delegation
04 “被拒绝的遗赠”碰到就需要重构吗?
并不是。是否重构掉“被拒绝的遗赠”的代码取决于受益的多少。
1,有的时候后“被拒绝的遗赠”并不会创建一些新的类型,而这些类型有时并不是业务中描述的,而是纯粹技术上的实现。例如上面的使用接口组合代替继承。直白的表达意图要比高度抽象的表达代码容易理解。
2,如果重构掉“被决绝的遗赠”问题会带啦大量的重复类,那么想象新的重构手法。
3,在阅读源码的时候,有时候也会发现源码中有“被拒绝的遗赠”Smell的代码,作者之所以保留,很可能是因为重构掉它会带来大量的修改,投入产出并不高。在《重构》中作者也会经常使用继承,大部分时间都能达到期望的效果,如果稍后修改,就会重构掉这种继承关系。时刻保持重构,保持代码的Simple Design。
05 继承有可能造成的问题
1,子类只能继承一个父类。导致行为的原因可能用多种,但是继承只能处理一个方向上的变化。 2,继承给类之间引入了非常紧密的关系。在父类上做任何修改,都有可能会影响子类的行为。所以在处理有积继承关系的代码的时候,要充分理解父类和子类的关系。
拒收的遗赠就是继承是容易出现的Code Smell。关于继承经常出现的Smell包括:
- 被拒绝的遗赠
- 不当的紧密性
- 慵懒类
本文将专注在被拒绝的遗赠问题上,对于不当的紧密性和慵懒类将在后续的文章中介绍清楚。
文章并没有按照《重构》中Smell的顺序整理,直接上来就是“Refused-Bequest”。后面会陆续整理一些其他Smell的代码和内容。
参考
01《重构》第一版
02《重构》第二版
03《重构手册》
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Dojo 国际化进阶
Dojo 官网:https://dojo.io 使用消息包 Dojo 的消息包概念是一个 Map 对象,其中存储用 key 标识的文本消息,每个 key 标识的消息内容可以用一到多种语言表示。 当需要向最终用户显示消息时,Dojo 应用程序使用 key 来引用该消息。这就避免了在代码中硬编码某一种语言的文本,而是提供了一门或多门语言的外部消息集,这些消息集能独立于应用程序的代码单独维护。 在渲染时,根据部件中引用消息 key 时的当前区域设置,Dojo 的 i18n 框架使用指定语言的文本内容替换掉消息 key。 Dojo 应用程序可以选择在整个应用程序中只使用一个消息包;也可以将消息进一步拆分的更细,接近于一个部件对应一个消息包,最终得到一个包含多个消息包的应用程序。 包的默认语言 每个消息包中都会包含多门语言的翻译。其中一门语言需要作为其余语言包的默认模块。这个默认的语言模块用于导入(或引用)包,主要实现两个需求: 提供一组完整的消息 key 及对应的内容(使用默认语言),如果包中的其他语言没有覆写某一个 key,则回退使用这些 key 和内容 列出包中支持的其他语言,以及每一个支...
- 下一篇
Unicode字符集的由来
本文起源于行走在阳光下的那些不可见字符中的知识边界,因为涉及到字符相关,而我也不能很清楚的描述Unicode的前世今生,故而有了此文。 计算机技术的革命极大地方便了人们的工作与生活,使得人类生活前进了一大步,可是在计算机发展进程中,世界各地由于语言文字不一,有过那么一段混乱难受的日子... 字符世界的起源 由于计算机在美国诞生,因此字符集最初也只考虑了美国人当时的需求,诞生了大家熟知的ASCII(American Standard Code for Information Interchange),它由26个基本拉丁字母、阿拉伯数字、英式标点符号和一些控制字符组成。 随着世界的发展,各国人民也开始接触计算机,然而各国人民也有自己独特的文化需求,最开始的字符集满足不了人们日益增长的需求,各国出现了不同的字符集标准,比如国内早期的Java程序员熟知的GBK、GB18030等,我曾经实习的时候接触的用JSP的老系统里就有相关的编码设定,如果用错误编码设定了文档解析格式,可能就会看到乱码,再想想世界上那么多个国家,会有多少编码标准啊,这也是互联网早期乱码比较多的原因吧。 各种编码的字符互不兼容...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
-
Docker使用Oracle官方镜像安装(12C,18C,19C)
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS8编译安装MySQL8.0.19
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- MySQL8.0.19开启GTID主从同步CentOS8
- CentOS7,8上快速安装Gitea,搭建Git服务器
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
推荐阅读
最新文章
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- MySQL8.0.19开启GTID主从同步CentOS8
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2整合Redis,开启缓存,提高访问速度
- Windows10,CentOS7,CentOS8安装Nodejs环境
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- CentOS7设置SWAP分区,小内存服务器的救世主