程序员首页投稿(暂停使用,暂停投稿)

阅读《重构》之一:重要的第一个例子

2017-09-10  本文已影响569人  FindCrt

第一个例子很重要,因为它通过实际操作带你走进什么是重构,为何重构可以带来实用的价值。作者也在书开头说了,理论容易让他昏昏入睡,一个好的例子能带来更好的理解,他做到了。

这是一个什么例子?

是一个用户租聘录像带的小程序,包含3个类:用户、租聘和录像带,它们的关系如下:customer --> rental --> movie。然后核心的业务是计算用户租聘的价格和加分,并生成订单输出。

最初始的代码是把这个生成订单的逻辑全部集中在一个statement函数里,内容大致如下:

public String statement(){
    double totalAmount = 0; //此次租聘总价
    int rentalPoints = 0;   //此次租聘积分
    String result = "Rental Record for xxx";//租聘订单内容
    
    for(each in rentals){ //(1)
        double thisAmount = 0;
        switch(each.getMovie().getPriceCode()){
            case xxx
            case xxx
            //根据影片类型计算价格
            thisAmount = xxx
        }
        
        //根据类型计算用户积分(2)
        rentalPoints += xxx
        
        //添加这个影片内容输入到订单(3)
        result += xxx
    }
}

首先这个程序并没有什么问题,看上去,但是为了让程序更健壮、更容易应对变化(这也是重构的最重要的目的之一),我们需要对未来可能的改变做一些假设判断。

首先分析一个整个业务,主要的内容为:

那么可能的改变就有:

这些都会导致statement这个函数的改变,然而它们却是不同动机引发的。为了让修改集中在更小的逻辑范围里,需要对statement进行拆分。

改进1

把对影片价格的计算移动到单独的函数里。也就是(1)位置switch部分。这一步就可以应对增加新类型或者旧类型价格计算方式改变。这些改变都会在单独的新函数里修改,而不会干扰到statement函数。

改进2

当把影片价格的计算移动到单独函数去之后,会发现这个函数并没有用到当前类customer的任何信息,它的逻辑完全是依赖于rental这个类的(一个rental代表一个影片的租聘,和影片是一对一的关系)。

这也是书里提及的最重要的重构标识之一:当一个函数更多的依赖于另一个类而不是当前类的时候,应该考虑把这个函数移动到那个依赖更多的类中。

所以在Rental类中添加double getCharge()函数,这样最开始的switch部分就改为了:
thisAmount = each.getCharge().

做完这一步,那么影片类型和价格计算的变化,不仅不会影响到statement函数,甚至不会影响到customer这个类。

改进3

积分计算和上面价格计算一样,也拆分到Rental类里面去。

改进4

书里接下来的改进是把statement函数里的循环都拆了,循环存在的目的是为计算总体的价格和积分,总价格计算放到一个新函数getTotalCharge里,总积分计算方法新函数getTotalFrequentRentalPoints里。

书里给的理由是减少临时变量,但我觉得不是重点,结合后面(p32)的这一句话:如果没有这些查询函数,其他函数就必须了解Rental类,并自行建立循环。这里有几点非常重要:

这一次改进后,任何其他地方需要用到总价格或总积分,它不需要知道任何细节,到底是循环还是不循环,各种价格如何计算或者哪些类型影片不计入积分等等,它只需要调用getTotalCharge,一切ok!

改进5

从改进2那里可知,其实仔细想,价格和积分的计算更多依赖于movie类,对rental的依赖只有租聘的天数,所以把它们进一步移动到movie才是对的。如rental类变为:

class Rental
double getCharge(){
    return _movie.getCharge(_daysRented);
}
改进6

接下来有两点改进:1. 引入state模式 2.使用多态代替switch.

什么是使用多态代替继承?

首先多态是obj.method1()会因为obj的类型不同而调用不同的方法,在计算影片的价格和积分时,同样因为影片类型不同而进行不同的操作,这正好符合多态的行为方式。

修改之前是:

class Movie
double getCharge(){
    switch(type){
        case 1:
            xxx
            break;
        case 2:
            xxx
            break;
        .....
    }
}

修改之后变为:

class Type1Movie
double getCharge(){
    type1的计算方式
}

class Type2Movie
double getCharge(){
    type2的计算方式
}
......

不同的计算方式分散到不同的子类里去了,而外界调用的时候却没有改变,还是movieObj.getCharge()

这种手段可以很好的应对新增或删除类型,新增类型只需要添加一个新类,实现getCharge方法,就一切正常运转了,原有的类甚至不知道新增或者删除了一个类。

这样就有了下图的结构

movie继承.JPG

然后是使用state模式,修改后的类图:

state模式movie继承.JPG

可以看到是: 把价格计算单独抽离做了新的类price,然后price根据不同计算方式构建继承体系。

state模式是设计模式那本书里提的,我的理解是:类的行为受到某个属性的影响,当这个影响变得复杂之后,比如要做许多的判断,可以把这个属性抽离作为状态类,把相关的行为搬移到状态类里。

其实从这里可以看出,继承也是可以达到减轻状态判断的,那么state模式的意义何在?这里有一个问答,虽然问的是继承和strategy模式的区别,但也可以理解到state模式上。简单说,如果一个类,有多种影响行为的属性,全部继承,那么子类数量将为相当巨大。比如属性1有4种状态,属性2有5种状态,那么子类就有20个了。而采用state模式,可以让各种state自由组合,更方便。

书里提到使用state模式的只有一句话:一步影片可以在生命周期内修改自己的分类,一个对象却不能在生命周期内修改自己所属的类。就是说影片的类型在逻辑上是变化的,而如果使用类继承策略,那么某个影片对象会因为无法修改自己的类而无法修改影片类型。

使用继承体系,那么逻辑上的类型就和程序里的类绑定了,而如果逻辑上是可变的,那么就产生了冲突。

而采用state模式,就可以化解这个问题,只需切换不同的属性对象,就拥有了不同的类型。就像。。。自行车装上了电动马达就变成了电动车了。

最后:movie对象拥有price对象,price根据计算方式拆分不同子类,使用多态进行不同方式价格计算。

最终类图.JPG
上一篇下一篇

猜你喜欢

热点阅读