您现在的位置是:首页 > 文章详情

重构分析21: 被拒绝的遗赠(Refused Bequest)

日期:2019-12-22点击:402

子类和父类的关系开始很简单,但是随着时间的推移有可能会变的越来越复杂。一个子类通常需要紧密的依赖其父类,但是有时会矫枉过正。

这就是继承的两面性,下面我们看看继承可能代码的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《重构手册》

原文链接:https://my.oschina.net/vvthink/blog/3145992
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章