代码重构(六):代码重构完整案例

无论做什么事情呢,都要善始善终呢。前边连续发表了5篇关于重构的博客,其中分门别类的介绍了一些重构手法。今天的这篇博客就使用一个完整的示例来总结一下之前的重构规则,也算给之前的关于重构的博客画一个句号。今天的示例借鉴于《重构,改善既有代码的设计》这本书中的第一章的示例,在其基础上做了一些修改。今天博客从头到尾就是一个完整的重构过程。首先会给出需要重构的代码,然后对其进行分析,然后对症下药,使用之前我们分享的重构规则对其进行一步步的重构。

先来聊一下该示例的使用场景(如果你有重构这本书的话,可以参加第一章中的示例,不过本博客中的示例与其有些出入)。就是一个客户去DVD出租的商店里进行消费,下方的程序是给店主用的,来根据用户所借的不同的DVD种类和数量来计算该用户消费的金额和积分。需求很简单而且也不难理解。今天博客会给出原始的代码,也是需要进行重构的代码。当然原始代码完全符合需求,并且可以正确执行。废话少说,先看示例吧。

 

一、需要重构的代码

在本篇博客的第一部分,我们先给出完成上述需求需要重构的代码。然后在此基础上进行分析,使用之前我们提到过的重构手法进行重构。首先我们给出了电影类的实现。在Movie类中有电影的种类(静态常量):普通电影、儿童电影、新电影,然后有两个成员变量/常量是priceCode(价格代码)、title(电影名称),最后就是我们的构造方法了。该Movie类比较简单,在此就不做过多的赘述了。

 

实现完Movie类接下来就是租赁类Rental,这个Rental类的职责就是负责统计某个电影租赁的时间。下方就是这个租赁类,该类也是比较简单的,其中有两个字段,一个是租了的电影,另一个就是租赁的时间了。

  

接下来要实现我们的消费者类了,也就是Customer类。在Customer类中有消费者的名字name和一个数组,该数组中寸的就是租赁电影的集合。其中的statement()方法就是结算该客户的结算信息的方法,并将结果进行打印。在此我们需要了解的需求是每种电影的计价方式以及积分的计算规则。

电影价格计算规则:

  普通片儿--2天之内含2天,每部收费2元,超过2天的部分每天收费1.5元

  新片儿--每天每部3元 

  儿童片--3天之内含3天,每部收费1.5元,超过3天的部分每天收费1.5元

积分计算规则:

       每借一步电影积分加1,新片每部加2

statement()函数中所做的事情就是根据上面的计算规则,根据用户所租赁的电影的不同来进行金额的计算和积分的计算的。

  

如果你看代码不太直观的话,下面我使用了startUML简单的画了一个UML的类图来说明上述三个类中的依赖关系。具体如下所示:

 

在对上面代码重构之前呢,我们还必须有上述代码的测试用例。因为在每次重构之前,我们修改的是代码的内部结构,而代码模块对外的调用方式不会变的。所以我们所创建的测试用例可以帮助验证我们重构后的程序是否可以正常的工作,是否重构后还符合我们的需求。下方就是我们创建的测试用例(当然,在iOS开发中你可以使用其他的测试框架来进行单元测试,重构时,单元测试是少不了的)。在本篇博客中重构后的代码仍然使用下方的测试用例。

//测试用例--------------------------------------------------------------------
//创建用户
let customer = Customer(name: "ZeluLi")

//创建电影
let regularMovie:Movie = Movie(title: "《老炮儿》", priceCode: Movie.REGULAR)
let newMovie:Movie = Movie(title: "《福尔摩斯》", priceCode: Movie.NEW_RELEASE)
let childrenMovie:Movie = Movie(title: "《葫芦娃》", priceCode: Movie.CHILDRENS)

//创建租赁数据
let rental1:Rental = Rental(movie: regularMovie, daysRented: 5)
let rental2:Rental = Rental(movie: newMovie, daysRented: 8)
let rental3:Rental = Rental(movie: childrenMovie, daysRented: 2)

customer.rentals.append(rental1)
customer.rentals.append(rental2)
customer.rentals.append(rental3)

let result = customer.statement()
print(result)

针对上述案例,上面测试用例的输出结果如下。在每次重构后,我们都会执行上述测试代码,然后观察结果是否与之前的相同。当然如果你的是单元测试的话,完全可以把对结果检查的工作交给单元测试中的断言来做。

    

 

二、重构1:对较statement函数进行拆分

1.对statement()函数使用“Extract Method”原则

在上面的案例中,最不能容忍的,也就是最需要重构的首先就是Customer中的statement()函数。statement()函数最大缺点就是函数里边做的东西太多,我们第一步需要做的就是对其进行拆分。也就是使用我们之前提到过的“Extract Method”(提炼函数)原则对该函数进行简化和拆分。将statement()中可以独立出来的模块进行提取。经过分析后的,我们不难发现下方红框当中的代码是一个完整的模块,一个是进行单价计算的,一个是进行积分计算的,我们可以将这两块代码进行提取并封装成一个新的方法。在封装新方法时,要给这个新的方法名一个恰当的函数名,见名知意。

   

下方这块代码就是我们对上面这两个红框中的代码的提取。在提取时,将依赖于statement()函数中的数据作为新函数的参数即可。封装后的方法如下,在statement函数中相应的地方调用下方的方法即可。下方就是我们封装的计算当前电影金额和计算积分的函数。这两个函数都需要传入一个Rental的对象。

//根据租赁订单,计算当前电影的金额
    func amountFor(aRental: Rental) -> Double {

        var result:Double = 0       //单价变量

        switch aRental.movie.priceCode {
            case Movie.REGULAR:
                result += 2
                if aRental.daysRented > 2 {
                    result += Double(aRental.daysRented - 2) * 1.5
                }
            case Movie.NEW_RELEASE:
                result += Double(aRental.daysRented * 3)
            case Movie.CHILDRENS:
                result += 1.5
                if aRental.daysRented > 3 {
                    result += Double(aRental.daysRented - 3) * 1.5
                }
            default:
                break
        }
        return result
    }

     //计算当前电影的积分
    func getFrequentRenterPoints(rental: Rental) -> Int {
        var frequentRenterPoints: Int = 0               //用户积分
        frequentRenterPoints++
        if rental.movie.priceCode == Movie.NEW_RELEASE &&
            rental.daysRented > 1{
                frequentRenterPoints++
        }
        return frequentRenterPoints
    }

经过上面的重构步骤,我们会运行一下测试用例或者执行一下单元测试,看是否我们的重构过程引起了新的bug。

 

三、重构2:将相应的方法移到相应的类中

经过上面的重构,我们从statement()函数中提取了两个方法。观察这两个重构后的方法我们不难看出,这两个封装出来的新的方法都需要一个参数,这个参数就是Rental类的对象。也就是这两个方法都依赖于Rental类,而对该函数所在的当前类不太感冒。出现这种情况的原因就是这两个函数放错了地方,因为这两个函数放在Customer类中不依赖与Customer类而依赖于Rental类,那就足以说明这两个方法应该放在Rental类中。

经过我们简单的分析后,我们就可以决定要将我们新提取的方法放到Rental类中,并且函数的参数去掉。因为函数在Rental类中,所以在函数中直接使用self即可。将计算金额的方法和计算积分的方法移到Rental类中后,我们的Rental类如下所示。在我们的Customer中的statement()方法中在计算金额和计算积分时,直接调用Rental中的方法即可。经过这一步重构后,不要忘记执行一下你的测试用例,监测一下重构的结果是否正确。

   

 

四、使用“以查询取代临时变量”再次对statement()函数进行重构

经过第二步和第三步的重构后,Customer中的statement()函数如下所示。在计算每部电影的金额和积分时,我们调用的是Rental类的对象的相应的方法。下方的方法与我们第一部分的方法相比可谓是简洁了许多,而且易于理解与维护。

   

不过上面的代码仍然有重构的空间,举个例子,如果我们要将结果以HTML的形式进行组织的话,我们需要将上面的代码进行复制,然后修改result变量的文本组织方式即可。但是这样的话,其中的好多临时变量也需要被复制一份,这是完全相同的,这样就容易产生重复的代码。在这种情况下,我们需要使用“Replace Temp with Query”(已查询取代临时变量)的重构手法来取出上面红框中的临时变量。

上面红框中的每个临时变量我们都会提取出一个查询方法,下方是使用“Replace Temp with Query”(已查询取代临时变量)规则重构后的statement()函数,以及提取的两个查询函数。

   

经过上面这些步骤的重构,我们的测试用例依然不变。在每次重构后我们都需要调用上述的测试用例来检查重构是否产生了副作用。现在我们的类间的依赖关系没怎么发生变化,只是相应类中的方法有些变化。下方是现在代码所对应的类图,因为在上述重构的过程中我们主要做的是对函数的重构,也就是对函数进行提取,然后将提取的函数放到相应的类中,从下方的简化的类图中就可以看出来了。

   

 

五. 继续将相应的函数进行移动(Move Method)

对重构后的代码进行观察与分析,我们任然发现在Rental类中的getCharge()函数中的内容与getFrequentRenterPoints()函数中的内容对Movie类的依赖度更大。因为这两个函数都只用到了Rental类中的daysRented属性,而多次用到了Movie中的内容。因此我们需要将这两个函数中的内容移到Movie类中更为合适。所以我继续讲该部分内容进行移动。

移动的方法是保留Rental中这两个函数的声明,在Movie中创建相应的函数,将函数的内容移到Movie中后,再Rental中调用Movie中的方法。下方是我们经过这次重构后我们Movie类中的内容。其中红框中的内容是我们移过来的内容,而绿框中的参数需要从外界传入。

     

将相应的方法体移动Movie类中后,在Rental中我们需要对其进行调用。在调用相应的方法时传入相应的参数即可。下方就是经过这次中国Rental类的代码,绿框中的代码就是对Movie中新添加的方法的调用。

     

经过上面的重构,我们的方法似乎是找到了归宿了。重构就是这样,一步步来,不要着急,没动一步总是要向着好的方向发展。如果你从第一部分中的代码重构到第五部分,似乎有些困难。经过上面这些间接的过程,感觉也是挺愉快的蛮。下方是经过我们这次重构的类图。

   

 

六、使用“多态”取代条件表达式

在我们之前的博客中对条件表达式进行重构时,提到了使用类的多态对条件表达式进行重构。接下来我们就要使用该规则对Movie类中的getCharge()与getFrequentRenterPoints()函数进行重构。也就是使用我们设计模式中经常使用的“状态模式”。在该部分我们不需要对Rental类和Customer类进行修改,只对Movie类修改,并且引入相应的接口和继承关系。

我们对Movie类中的getCharge()方法中的Switch-Case结构观察时,我们很容易发现,此处完全可以使用类的多态来替代(具体请参见《代码重构(四):条件表达式重构规则(Swift版)》)。具体实现方式是将不通的价格计算方式提取到我们新创建的价格类中,每种电影都有自己价格类,而这些价格类都实现同一个接口,这样一来在Movie中就可以使用多态来获取价格了。积分的计算也是一样的。下方是我们要实现结构的类图。下方红框中是在原来基础上添加的新的接口和类,将条件表达式所处理的业务逻辑放在了我们新添加的类中。这样我们就可以使用类的多态了,而且遵循了“单一职责”。

   

 

下方代码就是上面大的红框中所对应的代码实现。Price是我们定义好的协议,在协议中规定了遵循该协议的类要实现的方法。而在每个具体实现类中实现了相同的接口,但是不同的类中相同的方法做的事情不同。在不同的类中的getCharge()中要做的事情就是Switch-Case语句中所处理的数据。

   

添加上上面的结构以后,在么我们的Movie中就可以使用多态了,在Movie中添加了一个Price声明的对象,我们会根据不同的priceCode来给price变量分配不同的对象。而在getCharge()中只管调用price的getCharge()函数即可,具体做法如下。

   

 

今天的博客到这儿也就差不多了,其实上面的代码仍然有重构的空间,如果我们想把Switch-Case这个结构去掉的话,我们可以在上面代码的基础上创建多个工厂方法即可。在此就不过赘述了。

如果看完今天的博客的内容不够直观的话,那么请放心。本篇博客中每次重构过程的完整实例会在github上进行分享。对每次重构的代码都进行了系统的整理。今天博客中的代码整理的结果如下。

  

时间: 2024-10-03 02:33:05

代码重构(六):代码重构完整案例的相关文章

重构-使代码更简洁优美:实际经验之谈(提供一技巧,让你省掉N多代码)

这几天没怎么写文,因为在用 CYQ.Data  框架 重构以前的一个博客源码,而在重构的过程中,最关键的就是简化代码了.   今天,我将说一个很典型的示例,看完本示例后,不要惊讶,不要怀疑,它不是神马,也不是浮云,   而是很实在的一种方式,能让你节省了N多的代码,让你的代码看起来更简洁优美.   而这里说的一个很典型的示例,是从我目前重构中的博客中应用而来的:   一:正常的开发方式   1:扫一眼当前的项目解决方案   2:说说Module库 一般我们很常见的会有一个页面基类,当然各花叫法不

重构-使代码更简洁优美II:实际经验之谈(项目分层是怎么扯上代码节省的)

前言: 好几天没写文了,因为在折腾传说中的8国语言博客,实际目前预定义了10国+1自定义语言,代码还在慢慢的写着写着~~~~ 目前最新进展预览网址为:http://cyq.tupianshop.com/ ,其强大之处及 CYQ.Data 框架 V3.N 系列   后文再介绍了. 写文章有时候是需要有灵感或一时的冲动的~比如刚刚在改博客代码,经过一段思考,得到一些灵感,便有了此文.   在很久很久的 Long Long Ago 以前,写过一篇文章:重构-使代码更简洁优美:实际经验之谈(提供一技巧,

求基于小波的超分辨率重构matlab代码

问题描述 求基于小波的超分辨率重构matlab代码 给小波系数插值也行,在小波域进行字典学习也行.如果两种代码都有麻烦发给我

javascript-本人刚入门js,请问像如下的这段代码大致该怎么重构

问题描述 本人刚入门js,请问像如下的这段代码大致该怎么重构 var prepareCart = function (host, accessToken, username, state, count,filename) { if (count == 0) { completeTest(state); } else { log("INFO", "Preparing the cart"); request.post({ url: host + '/ls/api/tes

重构遗留程序的学习案例

遗留代码经常是腐臭的,每个优秀的开发者都想把它重构.而进行重构的一个理想的先决条件是,它应该包含一组单元测试用例,以避免产生回归缺陷.但是为遗留代码编写单元测试可不是件容易的事,因为它经常是一团糟.要想为遗留代码编写有效的单元测试,你大概得先把它重构一下.但要重构它,你又需要单元测试来确保你没有破坏任何功能.这种状况相当于要回答是先有鸡还是先有蛋.这篇文章通过分享一个我曾参与过的真实案例,描述了一种可以安全地重构遗留代码的方法. 问题描述 在这篇文章中,我将用一个真实案例来描述测试与重构遗留系统

MySpace的六次重构

做互联网,特别是做WEB2.0的兄弟姐妹们,下面这篇文章强烈推荐阅读: MySpace的六次重构: http://www.baselinemag.com/print_article2/0,1217,a=198614,00.asp 看完之后,你会发现MySpace真是一家伟大的公司. 只有真是怀有理想和信念的公司才会把自己的网站完全推倒六次,再重写六次. MySpace重写了六次,我建议大家都详细看这六次,为什么重构? 重构解决了什么问题? 之后又带来那些新问题? MySpace现在用的是100%

用JBuilder 2005实现重构之认识重构

为什么要重构 从Martin Fowler所著的<重构--改善既有代码的设计>一书连续两年成为最畅销的计算机图书之一,就可以知道重构给程序员所带来的欣喜程度了. 那么什么是重构呢?重构就是在不改变软件现有功能的基础上,通过调整程序代码改善软件的质量.性能,使其程序的设计模式和架构更趋合理,提高软件的扩展性和维护性. 也许有人会问,为什么不在项目开始时多花些时间把设计做好,而要以后花时间来重构呢?要知道一个完美得可以预见未来任何变化的设计,或一个灵活得可以容纳任何扩展的设计是不存在的.系统设计人

JBuilder2005实现重构之对重构的支持

Martin Flower在写<重构>时曾经感叹地说,如果有一个自动化的重构工具出现就好了,而且也预言了重构的发展方向是工具自动化重构.JBuilder正好迎合了这声呼喊,到目前为此,可以很公允地说,还没有一种工具在重构的表现上可望其项背. 1.提供了哪些重构的功能 JBuilderX(上一版本)就已经有了重构的功能,JBuilder 2005对代码重构投入了更多的热情,赋予了更多灵活易用的功能.在JBuilder 2005中,重构已经单独形成一个独立的Refactor主菜单.简要的讲JBui

JBuilder2005实现重构之杂项重构

1.优化import 简而言之,通过优化import的设置可以达到以下的目的: 去除无用的import语句:如在类中没有使用任何包中的类,则这个包的import语句可以删除. 设置包的阈值:当前类引用包中类的数目大于这个阈值时,引入整个包(如import java.io.*),否则为包中每个被引用的类单独指定的一个import语句(如import java.io.File). 设置包的排列顺序:按照一般的习惯,按包的常用程度从高到低进行排列,常用的包放在前面引入.一般情况下,JDK经典的包放在最