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

in 编程
关注公众号【好便宜】( ID:haopianyi222 ),领红包啦~
阿里云,国内最大的云服务商,注册就送数千元优惠券:https://t.cn/AiQe5A0g
腾讯云,良心云,价格优惠: https://t.cn/AieHwwKl
搬瓦工,CN2 GIA 优质线路,搭梯子、海外建站推荐: https://t.cn/AieHwfX9

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

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

关注公众号【好便宜】( ID:haopianyi222 ),领红包啦~
阿里云,国内最大的云服务商,注册就送数千元优惠券:https://t.cn/AiQe5A0g
腾讯云,良心云,价格优惠: https://t.cn/AieHwwKl
搬瓦工,CN2 GIA 优质线路,搭梯子、海外建站推荐: https://t.cn/AieHwfX9
扫一扫关注公众号添加购物返利助手,领红包
Comments are closed.

推荐使用阿里云服务器

超多优惠券

服务器最低一折,一年不到100!

朕已阅去看看