重构分析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
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成为兄弟子类,他们提供各自需要的方法。

重新整理继承关系.png

重构后的代码: https://gitlab.com/tengbai/refactoring/tree/21-refused-bequest-rebuild-mapping

2. 组合优于继承

在很多次的讨论中,都会提到使用接口组合来代替继承。下面的图显示使用接口组合来解决上面的遇到的“被拒绝的遗赠”的问题。

接口组合优于继承.png

重构后的代码: https://gitlab.com/tengbai/refactoring/tree/21-refused-bequest-refactoring-with-interface

3. 使用代理取代继承

将不同的变化原因委托给不同的类。委托是类之间的常规关系,使用委托接口更加清晰,耦合度更低。

上面的例子中使用委托来代替继承是最简单的一个修改方式。如下图:

使用委托代替继承.png

重构后的代码: 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《重构手册》

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容