1.2重构的第一步
每当要进行重构的时候,第一个步骤永远相同:即为将修改的代码建立一组可靠的测试环境,这些测试是必要的,因为尽管遵循重构手法可以使我避免绝大多数引入bug的情形,但我毕竟是人,毕竟有可能犯错,所以我需要可靠的测试。
接1.1,由于statement()的运作结果是个字符串,所以我首先假设一些客户,让他们每个人各租几部不同的影片,然后产生报表字符串,然后我就可以拿新的字符串和手上已经检查过的参考字符串做比较,运行这些测试只需要几秒钟,所以你会看到我经常运行他们。
测试过程中很重要的一部分,就是测试程序对于结果的报告方式,他们要么说“OK”,表示所有新字符串都和参考参数一样,要么就列出失败清单,显示问题字符串的出现行号。这些测试都能够自我检验。
进行重构的时候,我们需要依赖测试,让他告诉我们是否引入bug。好的测试是重构的根本。花时间建立一个优良的测试机制是完全值得的,因为当你修改程序时,好测试会给你必要的安全保障。测试机制在重构领域的地位实在太重要了。
1.3 分解并重组statement()
第一个明显一起我们注意的就是长得离谱的statement()。每当看到这样长长的函数,我就想把它大卸八块。要知道,代码块越小,代码的功能就愈容易管理,代码的处理和移动也就越轻松。
本章重构过程的第一个阶段中,我将说明如何把长长的函数切开,并把较小块的代码移至更合适的类。降低代码重复量,从而使新的(打印HTML格式详单的)函数更容易撰写。
第一步找出代码的逻辑泥潭并运用Extract Method(提炼函数)。本例一个明显的逻辑泥团就是switch语句(计算金额功能),把它提炼到独立函数中似乎比较好。
和任何重构手法一样,当我提炼一个函数时,我必须知道可能出什么错。如果提炼不好,就可能给程序引入bug。所以重构之前我们要先想出安全做法。可以参考重构列表中的安全步骤(后期补充)。
首先在这段代码里找出函数内的局部变量和参数。我们找到了两个,each(租赁实体对象)和thisAmount(某种影片的总金额数),前者并未被修改,后者会被修改。任何不会被修改的变量都可以当作参数传入新的函数,至于会被修改的变量就需要格外的小心。如果只有一个变量会被修改,可以把它当作返回值。thisAmount是个临时变量,其值在每次循环起始处被设为0,并且在switch语句之前不会改变,所以可以直接把新函数的返回值赋给它。
下面将展示重构前后的代码。重构前的代码在上,重构后的代码在下。凡是从函数提炼出来的代码,以及新代码所做的任何修改,只要不明显的都以粗体特别提醒。
原始代码
/**
* 顾客实体
* @author harry
*/
public class Customer {
private String _name;
private Vector<Rental> _rentals = new Vector<>();
public Customer(String _name) {
super();
this._name = _name;
}
public void addRental(Rental arg){
_rentals.addElement(arg);
}
public String getName() {
return _name;
}
public void setName(String _name) {
this._name = _name;
}
public String statement(){
double totalAmount = 0;//总金额
int frequentRenterPoints = 0;//本次总积分
Enumeration<Rental> rentals = _rentals.elements();
// 租赁备案
String result = "Rental Record for "+getName()+"\n";
while(rentals.hasMoreElements()){
double thisAmount = 0;
Rental each = rentals.nextElement();
// 计算金额
switch(each.get_movie().get_priceCode()){
case Movie.REGULAR:
thisAmount += 2;
if (each.get_dayRented() > 2) {
thisAmount += (each.get_dayRented()-2)*1.5;
}
break;
case Movie.NEW_RELEASE:
thisAmount += each.get_dayRented()*3;
break;
case Movie.CHILDRENS:
thisAmount += 1.5;
if (each.get_dayRented() > 3) {
thisAmount += (each.get_dayRented()-3)*1.5;
}
break;
}
// 常规积分累加
frequentRenterPoints++;
// 特殊新书积分计算
if (each.get_movie().get_priceCode() == Movie.NEW_RELEASE &&
each.get_dayRented() > 1) {
frequentRenterPoints++;
}
// 显示凭条
result += "\t"+each.get_movie().get_title()+"\t"+String.valueOf(thisAmount)+"\n";
totalAmount += thisAmount;
}
// 组装页脚
result += "Amount owed is "+String.valueOf(totalAmount)+"\n";
result += "You earned "+String.valueOf(frequentRenterPoints)+" frequent renter points";
return result;
}
}
重构后代码
/**
* 顾客实体
* @author harry
*/
public class Customer01 {
private String _name;
private Vector<Rental> _rentals = new Vector<>();
public Customer01(String _name) {
super();
this._name = _name;
}
public void addRental(Rental arg){
_rentals.addElement(arg);
}
public String getName() {
return _name;
}
public void setName(String _name) {
this._name = _name;
}
public String statement(){
double totalAmount = 0;//总金额
int frequentRenterPoints = 0;//本次总积分
Enumeration<Rental> rentals = _rentals.elements();
// 租赁备案
String result = "Rental Record for "+getName()+"\n";
while(rentals.hasMoreElements()){
double thisAmount = 0;
Rental each = rentals.nextElement();
// 计算金额
thisAmount = amountFor(each);
// 常规积分累加
frequentRenterPoints++;
// 特殊新书积分计算
if (each.get_movie().get_priceCode() == Movie.NEW_RELEASE &&
each.get_dayRented() > 1) {
frequentRenterPoints++;
}
// 显示凭条
result += "\t"+each.get_movie().get_title()+"\t"+String.valueOf(thisAmount)+"\n";
totalAmount += thisAmount;
}
// 组装页脚
result += "Amount owed is "+String.valueOf(totalAmount)+"\n";
result += "You earned "+String.valueOf(frequentRenterPoints)+" frequent renter points";
return result;
}
// 计算金额
private double amountFor(Rental each){
double thisAmount = 0;
switch(each.get_movie().get_priceCode()){
case Movie.REGULAR:
thisAmount += 2;
if (each.get_dayRented() > 2) {
thisAmount += (each.get_dayRented()-2)*1.5;
}
break;
case Movie.NEW_RELEASE:
thisAmount += each.get_dayRented()*3;
break;
case Movie.CHILDRENS:
thisAmount += 1.5;
if (each.get_dayRented() > 3) {
thisAmount += (each.get_dayRented()-3)*1.5;
}
break;
}
return thisAmount;
}
}
现在我们已经把原来的函数分为两块,可以分别处理它们。但amountFor()内的某些变量名称不太可爱,所以要修改掉,修改这些变量名是代码清晰的关键。
修改变量名后的代码:
private double amountFor(Rental aRental) {
double result = 0;
switch (aRental.get_movie().get_priceCode()) {
case Movie.REGULAR:
result += 2;
if (aRental.get_dayRented() > 2) {
result += (aRental.get_dayRented() - 2) * 1.5;
}
break;
case Movie.NEW_RELEASE:
result += aRental.get_dayRented() * 3;
break;
case Movie.CHILDRENS:
result += 1.5;
if (aRental.get_dayRented() > 3) {
result += (aRental.get_dayRented() - 3) * 1.5;
}
break;
}
return result;
}
最终UML如下图