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

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

这是一个什么例子?

是一个用户租聘录像带的小程序,包含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
    }
}

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

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

  • 计算价格
  • 计算积分
  • 生成订单内容,目前是纯文字类型。

那么可能的改变就有:

  • 来了新类型影片,价格和积分计算都不同已有类型
  • 已有类型的计算方式发生变化,比如店开不下去了,或者临时促销等等
  • 改用HTML方式输出订单,甚至改成生成图片发送给用户等等。

这些都会导致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类,并自行建立循环。这里有几点非常重要:

  • 首先根本的原因是需求。其他地方也需要总价格、总积分这些,比如你的程序有5个地方用到积分计算,而他们需要的都是总积分,那么一个单独的用来计算总积分的函数就变得非常需要。一个函数要还是不要,关键看需求。

  • 我一直认为模块封装就要像黑盒子一样。你提供了需要的函数,满足外界需要的任何需求,那么外界就不需要了解你的内部,对方也就能够安心的干自己的事。如果一个小需求需要你把整个程序的源码全部读一遍,那肯定是又浪费时间,又很容易干扰到其他部分。

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

推荐阅读更多精彩内容