《重构》C#版实现(二)抽取AmountFor方法

来源:互联网 发布:发发棋牌源码 编辑:程序博客网 时间:2024/05/16 19:44

上一篇讲了如何使用原始(待重构的)代码构建自动单元测试,以保障后面重构的进行。如果你跟着一步一步地做,会发现,自动测试绝对是重构过程必不可少的工具。那么现在开始真刀真枪地进行重构。

等等,在开始前的最后一刻,还需要提醒的是,也许你带着满腔热情,想把代码重构得优雅极致,但就像平时的工作一样,重构的过程更需要的是理性思考,而不是冲动。每一次重构实践,都应该包含了对设计、实现、可维护、可扩展性,以及成本的估算和权衡。

所以,首先看看对于Statement方法来说,从哪里入手比较合适?

public string Statement(){double totalAmount = 0;int frequentRenterPoints = 0;string result = "Rental Record for " + Name + "\n";foreach (Rental rental in Rentals){double thisAmount = 0;// determine amounts for each lineswitch (rental.Movie.PriceCode){case Movie.REGULAR:thisAmount += 2;if (rental.DaysRented > 2)thisAmount += (rental.DaysRented - 2) * 1.5;break;case Movie.NEW_RELEASE:thisAmount += rental.DaysRented * 3;break;case Movie.CHILDRENS:thisAmount += 1.5;if (rental.DaysRented > 3)thisAmount += (rental.DaysRented - 3) * 1.5;break;}// add frequent renter pointsfrequentRenterPoints++;// add bonus for a two day new release rentalif (rental.Movie.PriceCode == Movie.NEW_RELEASE &&rental.DaysRented > 1) frequentRenterPoints++;// show figures for this rentalresult += "\t" + rental.Movie.Title + "\t" + thisAmount.ToString() + "\n";totalAmount += thisAmount;}// add footer linesresult += "Amount owed is " + totalAmount.ToString() + "\n";result += "You earned " + frequentRenterPoints.ToString() + " frequent renter points";return result;}
实际上,应该换一个问题:对现在的程序来说,哪里最容易产生变化?毕竟,如果代码不会变化,重构就是多余的行为。优雅的代码只能满足程序员的审美需求,而不是客户对于功能的需求。
《重构》中指出:该程序最可能产生的变化有三点:
1.报告输出的类型可能变化,例如由普通字符串变成HTML格式的文本
2.计费方式可能发生变化
3.影片类型可能发生变化
无论哪一种变化,上面的Statement方法都不能很好地应对——它太胖了,涉及的逻辑、细节太多。所以,第一步,《重构》的作者选择将该方法中最长、并且同时涉及上述所有变化的,计算每一部影片花费的那个switch给抽取出去。
具体步骤是:
1.在Customer中新建一个计算花费的新方法AmountFor:
public int AmountFor(Rental rental){    return 0;}
2.把switch的代码copy(不是剪切)到AmountFor里:
public int AmountFor(Rental rental){switch (rental.Movie.PriceCode){case Movie.REGULAR:thisAmount += 2;if (rental.DaysRented > 2)thisAmount += (rental.DaysRented - 2) * 1.5;break;case Movie.NEW_RELEASE:thisAmount += rental.DaysRented * 3;break;case Movie.CHILDRENS:thisAmount += 1.5;if (rental.DaysRented > 3)thisAmount += (rental.DaysRented - 3) * 1.5;break;}return 0;}

3.但此时的代码是编译不了得,因为AmountFor中,thisAmount不存在。所以,在switch前面添加一个thisAmount的声明:

public int AmountFor(Rental rental){int thisAmount = 0;switch (rental.Movie.PriceCode){case Movie.REGULAR:thisAmount += 2;if (rental.DaysRented > 2)thisAmount += (rental.DaysRented - 2) * 1.5;break;case Movie.NEW_RELEASE:thisAmount += rental.DaysRented * 3;break;case Movie.CHILDRENS:thisAmount += 1.5;if (rental.DaysRented > 3)thisAmount += (rental.DaysRented - 3) * 1.5;break;}return thisAmount;}
4.此时,代码还是编译不了,因为花费计算时用到了小数,而thisAmount是整数。加上强制转换后:
public int AmountFor(Rental rental){int thisAmount = 0;switch (rental.Movie.PriceCode){case Movie.REGULAR:thisAmount += 2;if (rental.DaysRented > 2)thisAmount += (int)((rental.DaysRented - 2) * 1.5);break;case Movie.NEW_RELEASE:thisAmount += rental.DaysRented * 3;break;case Movie.CHILDRENS:thisAmount += (int)1.5;if (rental.DaysRented > 3)thisAmount += (int)((rental.DaysRented - 3) * 1.5);break;}return thisAmount;}
5.在认为新的方法完成后,去掉Statement中的switch代码块,变成:
public string Statement(){double totalAmount = 0;int frequentRenterPoints = 0;string result = "Rental Record for " + Name + "\n";foreach (Rental rental in Rentals){double thisAmount = AmountFor(rental);// add frequent renter pointsfrequentRenterPoints++;// add bonus for a two day new release rentalif (rental.Movie.PriceCode == Movie.NEW_RELEASE &&rental.DaysRented > 1) frequentRenterPoints++;// show figures for this rentalresult += "\t" + rental.Movie.Title + "\t" + thisAmount.ToString() + "\n";totalAmount += thisAmount;}// add footer linesresult += "Amount owed is " + totalAmount.ToString() + "\n";result += "You earned " + frequentRenterPoints.ToString() + " frequent renter points";return result;}
6.接着是非常重要的一步,运行上一篇中的单元测试项目Tests

7.意外发现,所有测试都失败了,查看StatementForCharles的错误消息如下:

Assert.AreEqual 失败。应为: <Rental Record for CharlesBraveHeart12GodFather6.5Amount owed is 18.5You earned 3 frequent renter points>,实际为: <Rental Record for CharlesBraveHeart12GodFather6Amount owed is 18You earned 3 frequent renter points>。
8.结果中发现,所有数值都是整数,而期待结果中包含小数。想起刚才我们为了让编译通过而添加的强制转型动作,可以据此合理推出问题就出在AmountFor的返回值类型上。遂更改AmountFor如下:
public double AmountFor(Rental rental){double thisAmount = 0;switch (rental.Movie.PriceCode){case Movie.REGULAR:thisAmount += 2;if (rental.DaysRented > 2)thisAmount += (rental.DaysRented - 2) * 1.5;break;case Movie.NEW_RELEASE:thisAmount += rental.DaysRented * 3;break;case Movie.CHILDRENS:thisAmount += 1.5;if (rental.DaysRented > 3)thisAmount += (rental.DaysRented - 3) * 1.5;break;}return thisAmount;}
9.运行单元测试项目,结果如下:


至此,一个简单的方法抽取(Extract Method)重构实践方算是完成了,总结的步骤如下:
1.思考现有代码的缺陷、确定重构的价值
2.从重构价值入手,确定重构入手点
3.在修改原方法前,先建立一个方法,该方法名称必须能表达被抽取内容的语义(概念)
4.将待抽取内容复制到新方法中,并根据需要添加局部变量的定义
5.去除原方法中的相关内容,替换成新方法的调用
6.运行单元测试
7.如果测试不通过,检查测试结果,或调试源代码,直到测试通过为止
8.重构完成
这个例子包含了作者刻意添加的一个小意外,主要是为了演示测试的重要性。但即使不考虑这种意外,上面的步骤还是十分繁复的,对于重构新手而言,一步一步跟着做可能收获会更大。除非对重构技术十分熟悉,否则不要轻易跳过上面的任何一步。一来,良好的基础和习惯是十分重要的。二来,每一步都有它的考虑,跳过任何一步意味着少了一份思考。