### 分解并重组statement()
第一个明显引起我注意的就是长得离谱的statement() 。每当看到这样长长的函数,我就想把它大卸八块。要知道,代码区块愈小,代码的功能就愈容易管理,代码的处理和搬移也都愈轻松。
本章重构过程的第一阶段中,我将说明如何把长长的函数切开,并把较小块的代码移至更合适的class 内。我希望降低代码重复量,从而使新的(打印HTML 报表用的)函数更容易撰写。
第一个步骤是找出代码的逻辑泥团(logical clump)并运用 Extract Method。本例一个明显的逻辑泥团就是switch 语句,把它提炼(extract)到独立函数中似乎比较好。
和任何重构准则一样,当我提炼一个函数时,我必须知道可能出什么错。如果我提炼得不好,就可能给程序引入臭虫。所以重构之前我需要先想出安全作法。由于先前我己经进行过数次这类重构,所以我已经把安全步骤记录于书后的重构名录(refactoring catalog)中了。
首先我得在这段代码里头找出函数内的局部变量(local variables)和参数(parameters)。我找到了两个:each 和thisAmount,前者并未被修改,后者会被修改。任何不会被修改的变量都可以被我当成参数传入新的函数,至于会被修改的变量就需格外小心。如果只有一个变量会被修改,我可以把它当作返回值。thisAmount 是个临时变量,其值在每次循环起始处被设为0,并且在switch 语句之前不会改变,所以我可以直接把新函数的返回值赋予它。
下面两页展示重构前后的代码。重构前的代码在左页,重构后的代码在右页。凡是从函数提炼出来的代码,以及新代码所做的任何修改,只要我觉得不是明显到可以一眼看出,就以粗体字标示出来特别提醒你。本章剩余部分将延续这种左右比对形式。
~~~
public String statement() {
double totalAmount = 0; //总消费金。
int frequentRenterPoints = 0; //常客积点
Enumeration rentals = _rentals.elements();
String result = "Rental Record for " + getName() + "\n";
while (rentals.hasMoreElements()) {
double thisAmount = 0;
Rental each = (Rental) rentals.nextElement(); //取得一笔租借记。
//determine amounts for each line
switch (each.getMovie().getPriceCode()) { //取得影片出租价格
case Movie.REGULAR: //普通片
thisAmount += 2;
if (each.getDaysRented() > 2)
thisAmount += (each.getDaysRented() - 2) * 1.5;
break;
case Movie.NEW_RELEASE: //新片
thisAmount += each.getDaysRented() * 3;
break;
case Movie.CHILDRENS: //儿童。
thisAmount += 1.5;
if (each.getDaysRented() > 3)
thisAmount += (each.getDaysRented() - 3) * 1.5;
break;
}
// add frequent renter points (累计常客积点。
frequentRenterPoints ++;
// add bonus for a two day new release rental
if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) &&
each.getDaysRented() > 1) frequentRenterPoints ++;
//show figures for this rental(显示此笔租借记录)
result += "\t" + each.getMovie().getTitle()+ "\t" +
String.valueOf(thisAmount) + "\n";
totalAmount += thisAmount;
}
//add footer lines(结尾打印)
result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
result += "You earned " + String.valueOf(frequentRenterPoints) +
" frequent renter points";
return result;
}
~~~
~~~
public String statement() {
double totalAmount = 0;
int frequentRenterPoints = 0;
Enumeration rentals = _rentals.elements();
String result = "Rental Record for " + getName() + "\n";
while (rentals.hasMoreElements()) {
double thisAmount = 0;
Rental each = (Rental) rentals.nextElement();
thisAmount = amountFor(each); //计算一笔租片费。
// add frequent renter points
frequentRenterPoints ++;
// add bonus for a two day new release rental
if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) &&
each.getDaysRented() > 1) frequentRenterPoints ++;
//show figures for this rental
result += "\t" + each.getMovie().getTitle()+ "\t" +
String.valueOf(thisAmount) + "\n";
totalAmount += thisAmount;
}
//add footer lines
result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
result += "You earned " + String.valueOf(frequentRenterPoints) +
" frequent renter points";
return result;
}
}
private int amountFor(Rental each) { //计算一笔租片费。
int thisAmount = 0;
switch (each.getMovie().getPriceCode()) {
case Movie.REGULAR: //普通片
thisAmount += 2;
if (each.getDaysRented() > 2)
thisAmount += (each.getDaysRented() - 2) * 1.5;
break;
case Movie.NEW_RELEASE: //新片
thisAmount += each.getDaysRented() * 3;
break;
case Movie.CHILDRENS: //儿童。
thisAmount += 1.5;
if (each.getDaysRented() > 3)
thisAmount += (each.getDaysRented() - 3) * 1.5;
break;
}
return thisAmount;
}
~~~
每次做完这样的修改之后,我都要编译并测试。这一次起头不算太好——测试失败了,有两笔测试数据告诉我发生错误。一阵迷惑之后我明白了自己犯的错误。我愚蠢地将amountFor() 的返回值型别声明为int,而不是double 。
~~~
private double amountFor(Rental each) { //计算一笔租片费。
double thisAmount = 0;
switch (each.getMovie().getPriceCode()) {
case Movie.REGULAR: //普通片
thisAmount += 2;
if (each.getDaysRented() > 2)
thisAmount += (each.getDaysRented() - 2) * 1.5;
break;
case Movie.NEW_RELEASE: //新片
thisAmount += each.getDaysRented() * 3;
break;
case Movie.CHILDRENS: //儿童。
thisAmount += 1.5;
if (each.getDaysRented() > 3)
thisAmount += (each.getDaysRented() - 3) * 1.5;
break;
}
return thisAmount;
}
~~~
我经常犯这种愚蠢可笑的错误,而这种错误往往很难发现。在这里,Java 无怨无尤地把double 型别转换为int 型别,而且还愉快地做了取整动作[Java Spec]。还好此处这个问题很容易发现,因为我做的修改很小,而且我有很好的测试。借着这个意外疏忽,我要阐述重构步骤的本质:由于每次修改的幅度都很小,所以任何错误都很容易发现。你不必耗费大把时间调试,哪怕你和我一样粗心。
TIP:重构技术系以微小的步伐修改程序。如果你犯下错误,很容易便可发现它。
由于我用的是Java ,所以我需要对代码做一些分析,决定如何处理局部变量。如果拥有相应的工具,这个工作就超级简单了。Smalltalk 也的确拥有这样的工具——Refactoring Browser。运用这个工具,重构过程非常轻松,我只需标示出需要重构的代码,在选单中点选Extract Method,输入新的函数名称,一切就自动搞定。而且工具决不会像我那样犯下愚蠢可笑的错误。我非常盼望早日出现Java 版本的重构工具!
现在,我已经把原本的函数分为两块,可以分别处理它们。我不喜欢amountFor() 内的某些变量名称,现在是修改它们的时候。
下面是原本的代码。
~~~
private int amountFor(Rental each) { //计算一笔租片费。
int thisAmount = 0;
switch (each.getMovie().getPriceCode()) {
case Movie.REGULAR: //普通片
thisAmount += 2;
if (each.getDaysRented() > 2)
thisAmount += (each.getDaysRented() - 2) * 1.5;
break;
case Movie.NEW_RELEASE: //新片
thisAmount += each.getDaysRented() * 3;
break;
case Movie.CHILDRENS: //儿童。
thisAmount += 1.5;
if (each.getDaysRented() > 3)
thisAmount += (each.getDaysRented() - 3) * 1.5;
break;
}
return thisAmount;
}
~~~
下面是易名后的代码:
~~~
private double amountFor(Rental aRental) { //计算一笔租片费。
double result = 0;
switch (aRental.getMovie().getPriceCode()) {
case Movie.REGULAR: //普通片
result += 2;
if (aRental.getDaysRented() > 2)
result += (aRental.getDaysRented() - 2) * 1.5;
break;
case Movie.NEW_RELEASE: //新片
result += aRental.getDaysRented() * 3;
break;
case Movie.CHILDRENS: //儿童。
result += 1.5;
if (aRental.getDaysRented() > 3)
result += (aRental.getDaysRented() - 3) * 1.5;
break;
}
return result;
}
~~~
易名之后我需要重新编译并测试,确保没有破坏任何东西。
更改变量名称是值得的行为吗?绝对值得。好的代码应该清楚表达出自己的功能,变量名称是代码清晰的关键。如果为了提高代码的清晰度,需要修改某些东西的名字,大胆去做吧。只要有良好的查找丨替换工具,更改名称并不困难。语言所提供的强型别检验(strong typing)以及你自己的测试机制会指出任何你遗漏的东西。记住:
TIP:任何一个傻瓜都能写出计算机可以理解的代码。惟有写出人类容易理解的代码,才是优秀的程序员。
代码应该表现自己的目的,这一点非常重要。阅读代码的时候,我经常进行重构。这样,随着对程序的理解逐渐加深,我也就不断地把这些理解嵌入代码中,这么一来才不会遗忘我曾经理解的东西。
**搬移「金额计算」代码**
观察amountFor() 时,我发现这个函数使用了来自Rental class 的信息,却没有使 用来自Customer class 的信息。
~~~
class Customer...
private double amountFor(Rental aRental) {
double result = 0;
switch (aRental.getMovie().getPriceCode()) {
case Movie.REGULAR:
result += 2;
if (aRental.getDaysRented() > 2)
result += (aRental.getDaysRented() - 2) * 1.5;
break;
case Movie.NEW_RELEASE:
result += aRental.getDaysRented() * 3;
break;
case Movie.CHILDRENS:
result += 1.5;
if (aRental.getDaysRented() > 3)
result += (aRental.getDaysRented() - 3) * 1.5;
break;
}
return result;
}
~~~
这立刻使我怀疑它是否被放错了位置。绝大多数情况下,函数应该放在它所使用的数据的所属object(或说class)内,所以amountFor() 应该移到Rental class 去。为了这么做,我要运用Move Method。首先把代码拷贝到Rental class 内, 调整代码使之适应新家,然后重新编译。像下面这样。
~~~
class Rental...
double getCharge() {
double result = 0;
switch (getMovie().getPriceCode()) {
case Movie.REGULAR:
result += 2;
if (getDaysRented() > 2)
result += (getDaysRented() - 2) * 1.5;
break;
case Movie.NEW_RELEASE:
result += getDaysRented() * 3;
break;
case Movie.CHILDRENS:
result += 1.5;
if (getDaysRented() > 3)
result += (getDaysRented() - 3) * 1.5;
break;
}
return result;
}
~~~
在这个例子里,「适应新家」意味去掉参数。此外,我还要在搬移的同时变更函数名称。
现在我可以测试新函数是否正常工作。只要改变Customer.amountFor() 函数内容,使它委托(delegate)新函数即可。
~~~
class Customer...
private double amountFor(Rental aRental) {
return aRental.getCharge();
}
~~~
现在我可以编译并测试,看看有没有破坏了什么东西。
下一个步骤是找出程序中对于旧函数的所有引用(reference)点,并修改它们,让它们改用新函数。
下面是原本的程序。
~~~
class Customer...
public String statement() {
double totalAmount = 0;
int frequentRenterPoints = 0;
Enumeration rentals = _rentals.elements();
String result = "Rental Record for " + getName() + "\n";
while (rentals.hasMoreElements()) {
double thisAmount = 0;
Rental each = (Rental) rentals.nextElement();
thisAmount = amountFor(each);
// add frequent renter points
frequentRenterPoints ++;
// add bonus for a two day new release rental
if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) &&
each.getDaysRented() > 1) frequentRenterPoints ++;
//show figures for this rental
result += "\t" + each.getMovie().getTitle()+ "\t" +
String.valueOf(thisAmount) + "\n";
totalAmount += thisAmount;
}
//add footer lines
result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
result += "You earned " + String.valueOf(frequentRenterPoints) +
" frequent renter points";
return result;
}
~~~
本例之中,这个步骤很简单,因为我才刚刚产生新函数,只有一个地方使用了它。一般情况下你得在可能运用该函数的所有classes 中查找一遍。
~~~
class Customer
public String statement() {
double totalAmount = 0;
int frequentRenterPoints = 0;
Enumeration rentals = _rentals.elements();
String result = "Rental Record for " + getName() + "\n";
while (rentals.hasMoreElements()) {
double thisAmount = 0;
Rental each = (Rental) rentals.nextElement();
thisAmount = each.getCharge();
// add frequent renter points
frequentRenterPoints ++;
// add bonus for a two day new release rental
if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) &&
each.getDaysRented() > 1) frequentRenterPoints ++;
//show figures for this rental
result += "\t" + each.getMovie().getTitle()+ "\t" +
String.valueOf(thisAmount) + "\n";
totalAmount += thisAmount;
}
//add footer lines
result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
result += "You earned " + String.valueOf(frequentRenterPoints) +
" frequent renter points";
return result;
}
~~~
做完这些修改之后(图1.3),下一件事就是去掉旧函数。编译器会告诉我是否我漏掉了什么。然后我进行测试,看看有没有破坏什么东西。
![](https://box.kancloud.cn/2016-08-15_57b1b4f1a15d7.gif)
图1.3 搬移「金额计算」函数后,所有classes 的状态(state)
有时候我会保留旧函数,让它调用新函数。如果旧函数是一个public 函数,而我又不想修改其他class 的接口,这便是一种有用的手法。
当然我还想对Rental.getCharge() 做些修改,不过暂时到此为止,让我们回。
~~~
public String statement() {
double totalAmount = 0;
int frequentRenterPoints = 0;
Enumeration rentals = _rentals.elements();
String result = "Rental Record for " + getName() + "\n";
while (rentals.hasMoreElements()) {
double thisAmount = 0;
Rental each = (Rental) rentals.nextElement();
thisAmount = each.getCharge();
// add frequent renter points
frequentRenterPoints ++;
// add bonus for a two day new release rental
if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) &&
each.getDaysRented() > 1) frequentRenterPoints ++;
//show figures for this rental
result += "\t" + each.getMovie().getTitle()+ "\t" +
String.valueOf(thisAmount) + "\n";
totalAmount += thisAmount;
}
//add footer lines
result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
result += "You earned " + String.valueOf(frequentRenterPoints) +
" frequent renter points";
return result;
}
~~~
下一件引我注意的事是:thisAmount 如今变成多余了。它接受each.charge 的执行结果,然后就不再有任何改变。所以我可以运用 Replace Temp with Query 除去。
~~~
public String statement() {
double totalAmount = 0;
int frequentRenterPoints = 0;
Enumeration rentals = _rentals.elements();
String result = "Rental Record for " + getName() + "\n";
while (rentals.hasMoreElements()) {
Rental each = (Rental) rentals.nextElement();
// add frequent renter points
frequentRenterPoints ++;
// add bonus for a two day new release rental
if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) &&
each.getDaysRented() > 1) frequentRenterPoints ++;
//show figures for this rental
result += "\t" + each.getMovie().getTitle()+ "\t" + String.valueOf (each.getCharge()) + "\n";
totalAmount += each.getCharge();
}
//add footer lines
result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
result += "You earned " + String.valueOf(frequentRenterPoints)
+ " frequent renter points";
return result;
}
}
~~~
做完这份修改,我立刻编译并测试,保证自己没有破坏任何东西。
我喜欢尽量除去这一类临时变量。临时变量往往形成问题,它们会导致大量参数被传来传去,而其实完全没有这种必要。你很容易失去它们的踪迹,尤其在长长的函数之中更是如此。当然我这么做也需付出性能上的代价,例如本例的费用就被计算了两次。但是这很容易在Rental class 中被优化。而且如果代码有合理的组织和管理,优化会有很好的效果。我将在p.69的「重构与性能」一节详谈这个问题。
**提炼「常客积点计算」代码**
下一步要对「常客积点计算」做类似处理。点数的计算视影片种类而有不同,不过不像收费规则有那么多变化。看来似乎有理由把积点计算责任放在Rental class 身上。首先我们需要针对「常客积点计算」这部分代码(以下粗体部分)运用 Extract Method 重构准则。
再一次我又要寻找局部变量。这里再一次用到了each ,而它可以被当作参数传入新函数中。另一个临时变量是frequentRenterPoints。本例中的它在被使用之前已经先有初值,但提炼出来的函数并没有读取该值,所以我们不需要将它当作参数传进去,只需对它执行「附添赋值动作」(appending assignment,operator+=)就行了。
我完成了函数的提炼,重新编译并测试;然后做一次搬移,再编译、再测试。重构时最好小步前进,如此一来犯错的几率最小。
~~~
public String statement() {
double totalAmount = 0;
int frequentRenterPoints = 0;
Enumeration rentals = _rentals.elements();
String result = "Rental Record for " + getName() + "\n";
while (rentals.hasMoreElements()) {
Rental each = (Rental) rentals.nextElement();
// add frequent renter points
frequentRenterPoints ++;
// add bonus for a two day new release rental
if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE)
&& each.getDaysRented() > 1) frequentRenterPoints ++;
//show figures for this rental
result += "\t" + each.getMovie().getTitle()+ "\t" + String.valueOf(each.getCharge())
+ "\n";
totalAmount += each.getCharge();
}
//add footer lines
result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
result += "You earned " + String.valueOf(frequentRenterPoints)
+ " frequent renter points";
return result;
}
}
~~~
~~~
class Customer...
public String statement() {
double totalAmount = 0;
int frequentRenterPoints = 0;
Enumeration rentals = _rentals.elements();
String result = "Rental Record for " + getName() + "\n";
while (rentals.hasMoreElements()) {
Rental each = (Rental) rentals.nextElement();
frequentRenterPoints += each.getFrequentRenterPoints();
//show figures for this rental
result += "\t" + each.getMovie().getTitle()+ "\t" +
String.valueOf(each.getCharge()) + "\n";
totalAmount += each.getCharge();
}
//add footer lines
result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
result += "You earned " + String.valueOf(frequentRenterPoints) +
" frequent renter points";
return result;
}
class Rental...
int getFrequentRenterPoints() {
if ((getMovie().getPriceCode() == Movie.NEW_RELEASE) && getDaysRented() > 1)
return 2;
else
return 1;
}
~~~
我利用重构前后的UML(Unified Modeling Language ,统一建模语言)图形(图1.4 至图1.7〕总结刚才所做的修改。和先前一样,左页是修改前的图,右页是修改后的图。
![](https://box.kancloud.cn/2016-08-15_57b1b4f1b7914.gif)
图1.4 [常客积点计算」函数被提炼及搬移之前的class diagrams
![](https://box.kancloud.cn/2016-08-15_57b1b4f1cda78.gif)
图1.5 「常客积点计算」函数被提炼及搬移之前的sequence diagrams
![](https://box.kancloud.cn/2016-08-15_57b1b4f1dfbdc.gif)
图1.6 「常客积点计算」函数被提炼及搬移之后的class diagrams
![](https://box.kancloud.cn/2016-08-15_57b1b4f1f34e5.gif)
图1.7 「常客积点计算」函数被提炼及搬移之后的sequence diagrams
**去除临时变量**
正如我在前面提过的,临时变量可能是个问题。它们只在自己所属的函数中有效,所以它们会助长「冗长而复杂」的函数。这里我们有两个临时变量,两者都是用来从Customer 对象相关的Rental 对象中获得某个总量。不论ASCII 版或HTML 版都需要这些总量。我打算运用 Replace Temp with Query,并利用所谓的query method 来取代totalAmount 和frequentRentalPoints 这两个临时变量。由于class 内的任何函数都可以取用(调用)上述所谓query methods ,所以它能够促进较干净的设计,而非冗长复杂的函数:
~~~
class Customer...
public String statement() {
double totalAmount = 0;
int frequentRenterPoints = 0;
Enumeration rentals = _rentals.elements();
String result = "Rental Record for " + getName() + "\n";
while (rentals.hasMoreElements()) {
Rental each = (Rental) rentals.nextElement();
frequentRenterPoints += each.getFrequentRenterPoints();
//show figures for this rental
result += "\t" + each.getMovie().getTitle()+ "\t" +
String.valueOf(each.getCharge()) + "\n";
totalAmount += each.getCharge();
}
//add footer lines
result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
result += "You earned " + String.valueOf(frequentRenterPoints) +
" frequent renter points";
return result;
}
~~~
首先我以Customer class 的getTotalCharge() 取代totalAmount 。
~~~
class Customer...
public String statement() {
int frequentRenterPoints = 0;
Enumeration rentals = _rentals.elements();
String result = "Rental Record for " + getName() + "\n";
while (rentals.hasMoreElements()) {
Rental each = (Rental) rentals.nextElement();
frequentRenterPoints += each.getFrequentRenterPoints();
//show figures for this rental
result += "\t" + each.getMovie().getTitle()+ "\t" +
String.valueOf(each.getCharge()) + "\n";
}
//add footer lines
result += "Amount owed is " + String.valueOf(getTotalCharge()) + "\n";
result += "You earned " + String.valueOf(frequentRenterPoints) +
" frequent renter points";
return result;
}
//译注:此即所谓query method
private double getTotalCharge() {
double result = 0;
Enumeration rentals = _rentals.elements();
while (rentals.hasMoreElements()) {
Rental each = (Rental) rentals.nextElement();
result += each.getCharge();
}
return result;
}
~~~
这并不是 Replace Temp with Query的最简单情况。由于totalAmount 在循环内部内赋值,我不得不把循环复制到query method 中。
重构之后,重新编译并测试,然后以同样手法处理frequentRenterPoints。
~~~
class Customer...
public String statement() {
int frequentRenterPoints = 0;
Enumeration rentals = _rentals.elements();
String result = "Rental Record for " + getName() + "\n";
while (rentals.hasMoreElements()) {
Rental each = (Rental) rentals.nextElement();
frequentRenterPoints += each.getFrequentRenterPoints();
//show figures for this rental
result += "\t" + each.getMovie().getTitle()+ "\t" +
String.valueOf(each.getCharge()) + "\n";
}
//add footer lines
result += "Amount owed is " + String.valueOf(getTotalCharge()) + "\n";
result += "You earned " + String.valueOf(frequentRenterPoints) +
" frequent renter points";
return result;
}
~~~
~~~
public String statement() {
Enumeration rentals = _rentals.elements();
String result = "Rental Record for " + getName() + "\n";
while (rentals.hasMoreElements()) {
Rental each = (Rental) rentals.nextElement();
//show figures for this rental
result += "\t" + each.getMovie().getTitle()+ "\t" +
String.valueOf(each.getCharge()) + "\n";
}
//add footer lines
result += "Amount owed is " + String.valueOf(getTotalCharge()) + "\n";
result += "You earned " + String.valueOf(getTotalFrequentRenterPoints()) +
" frequent renter points";
return result;
}
//译注:此即所谓query method
private int getTotalFrequentRenterPoints(){
int result = 0;
Enumeration rentals = _rentals.elements();
while (rentals.hasMoreElements()) {
Rental each = (Rental) rentals.nextElement();
result += each.getFrequentRenterPoints();
}
return result;
}
~~~
图1.8至图1.11分别以UML class diagram(类图)和interaction diagram (交互作用图)展示statement() 重构前后的变化。
![](https://box.kancloud.cn/2016-08-15_57b1b4f2164ab.gif)
图1.8 「总量计算」函数被提炼前的class diagram
![](https://box.kancloud.cn/2016-08-15_57b1b4f22b143.gif)
图1.9 「总量计算」函数被提炼前的sequence diagram
![](https://box.kancloud.cn/2016-08-15_57b1b4f2403ab.gif)
图1.10 「总量计算」函数被提炼后的class diagram
![](https://box.kancloud.cn/2016-08-15_57b1b4f253a53.gif)
图1.11 「总量计算」函数被提炼后的sequence diagram
做完这次重构,有必要停下来思考一下。大多数重构都会减少代码总量,但这次却增加了代码总量,那是因为Java 1.1需要大量语句(statements)来设置一个总和(summing)循环。哪怕只是一个简单的总和循环,每个元素只需一行代码,外围的支持代码也需要六行之多。这其实是任何程序员都熟悉的习惯写法,但代码数量还是太多了。
这次重构存在另一个问题,那就是性能。原本代码只执行while 循环一次,新版本要执行三次。如果循环耗时很多,就可能大大降低程序的性能。单单为了这个原因,许多程序员就不愿进行这个重构动作。但是请注意我的用词:如果和可能。除非我进行评测(profile),否则我无法确定循环的执行时间,也无法知道这个循环是否被经常使用以至于影响系统的整体性能。重构时你不必担心这些,优化时你才需要担心它们,但那时候你已处于一个比较有利的位置,有更多选择可以完成有效优化(见p.69的讨论)。
现在,Customer class 内的任何代码都可以取用这些query methods 了,如果系统他处需要这些信息,也可以轻松地将query methods 加入Customer class 接口。如果没有这些query methods ,其他函数就必须了解Rental class,并自行建立循环。在一个复杂系统中,这将使程序的编写难度和维护难度大大增加。
你可以很明显看出来,htmlStatement() 和statement() 是不同的。现在,我应该脱下「重构」的帽子,戴上「添加功能」的帽子。我可以像下面这样编写htmlStatement() ,并添加相关测试。
~~~
public String htmlStatement() {
Enumeration rentals = _rentals.elements();
String result = "<H1>Rentals for <EM>" + getName() + "</EM></ H1><P>\n";
while (rentals.hasMoreElements()) {
Rental each = (Rental) rentals.nextElement();
//show figures for each rental
result += each.getMovie().getTitle()+ ": " +
String.valueOf(each.getCharge()) + "<BR>\n";
}
//add footer lines
result += "<P>You owe <EM>" + String.valueOf(getTotalCharge()) + "</EM><P>\n";
result += "On this rental you earned <EM>" +
String.valueOf(getTotalFrequentRenterPoints()) +
"</EM> frequent renter points<P>";
return result;
}
~~~
通过计算逻辑的提炼,我可以完成一个htmlStatement() ,并复用(reuse)原本statements() 内的所有计算。我不必剪剪贴贴,所以如果计算规则发生改变,我只需在程序中做一处修改。完成其他任何类型的报表也都很快而且很容易。这次重构并不花很多时间,其中大半时间我用来弄清楚代码所做的事,而这是我无论如何都得做的。
前述有些重构码系从ASCII 版本里头拷贝过来——主要是循环设置部分。更深入的重构动作可以清除这些重复代码。我可以把处理表头(header)、表尾(footer)和报表细目的代码都分别提炼目出来。在 Form Template Method 实例中,.你可以看到如何做这些动作。但是,现在用户又开始嘀咕了,他们准备修改影片分类规则。我们尚未清楚他们想怎么做,但似乎新分类法很快就要引入,现有的分类法马上就要变更。与之相应的费用计算方式和常客积点计算方式都还待决定,现在就对程序做修改,肯定是愚蠢的。我必须进入费用计算和常客积点计算中,把「因条件 而异的代码」(译注:指的是switch 语句内的case 子句)替换掉,这样才能为 将来的改变镀上一层保护膜。现在,请重新戴回「重构」这顶帽子。
- 译序 by 侯捷
- 译序 by 熊节
- 序言
- 前言
- 章节一 重构,第一个案例
- 起点
- 重构的第一步
- 分解并重组statement()
- 运用多态(Polymorphism)取代与价格相关的条件逻辑
- 结语
- 章节二 重构原则
- 何谓重构
- 为何重构
- 「重构」助你找到臭虫(bugs)
- 何时重构
- 怎么对经理说?
- 重构的难题
- 重构与设计
- 重构与性能(Performance)
- 重构起源何处?
- 章节三 代码的坏味道
- Duplicated Code(重复的代码)
- Long Method(过长函数)
- Large Class(过大类)
- Long Parameter List(过长参数列)
- Divergent Change(发散式变化)
- Shotgun Surgery(散弹式修改)
- Feature Envy(依恋情结)
- Data Clumps(数据泥团)
- Primitive Obsession(基本型别偏执)
- Switch Statements(switch惊悚现身)
- Parallel Inheritance Hierarchies(平行继承体系)
- Lazy Class(冗赘类)
- Speculative Generality(夸夸其谈未来性)
- Temporary Field(令人迷惑的暂时值域)
- Message Chains(过度耦合的消息链)
- Middle Man(中间转手人)
- Inappropriate Intimacy(狎昵关系)
- Alternative Classes with Different Interfaces(异曲同工的类)
- Incomplete Library Class(不完美的程序库类)
- Data Class(纯稚的数据类)
- Refused Bequest(被拒绝的遗贈)
- Comments(过多的注释)
- 章节四 构筑测试体系
- 自我测试代码的价值
- JUnit测试框架
- 添加更多测试
- 章节五 重构名录
- 重构的记录格式
- 寻找引用点
- 这些重构准则有多成熟
- 章节六 重新组织你的函数
- Extract Method(提炼函数)
- Inline Method(将函数内联化)
- Inline Temp(将临时变量内联化)
- Replace Temp with Query(以查询取代临时变量)
- Introduce Explaining Variable(引入解释性变量)
- Split Temporary Variable(剖解临时变量)
- Remove Assignments to Parameters(移除对参数的赋值动作)
- Replace Method with Method Object(以函数对象取代函数)
- Substitute Algorithm(替换你的算法)
- 章节七 在对象之间搬移特性
- Move Method(搬移函数)
- Move Field(搬移值域)
- Extract Class(提炼类)
- Inline Class(将类内联化)
- Hide Delegate(隐藏「委托关系」)
- Remove Middle Man(移除中间人)
- Introduce Foreign Method(引入外加函数)
- Introduce Local Extension(引入本地扩展)
- 章节八 重新组织数据
- Self Encapsulate Field(自封装值域)
- Replace Data Value with Object(以对象取代数据值)
- Change Value to Reference(将实值对象改为引用对象)
- Replace Array with Object(以对象取代数组)
- Replace Array with Object(以对象取代数组)
- Duplicate Observed Data(复制「被监视数据」)
- Change Unidirectional Association to Bidirectional(将单向关联改为双向)
- Change Bidirectional Association to Unidirectional(将双向关联改为单向)
- Replace Magic Number with Symbolic Constant(以符号常量/字面常量取代魔法数)
- Encapsulate Field(封装值域)
- Encapsulate Collection(封装群集)
- Replace Record with Data Class(以数据类取代记录)
- Replace Type Code with Class(以类取代型别码)
- Replace Type Code with Subclasses(以子类取代型别码)
- Replace Type Code with State/Strategy(以State/strategy 取代型别码)
- Replace Subclass with Fields(以值域取代子类)
- 章节九 简化条件表达式
- Decompose Conditional(分解条件式)
- Consolidate Conditional Expression(合并条件式)
- Consolidate Duplicate Conditional Fragments(合并重复的条件片段)
- Remove Control Flag(移除控制标记)
- Replace Nested Conditional with Guard Clauses(以卫语句取代嵌套条件式)
- Replace Conditional with Polymorphism(以多态取代条件式)
- Introduce Null Object(引入Null 对象)
- Introduce Assertion(引入断言)
- 章节十一 处理概括关系
- Pull Up Field(值域上移)
- Pull Up Method(函数上移)
- Pull Up Constructor Body(构造函数本体上移)
- Push Down Method(函数下移)
- Push Down Field(值域下移)
- Extract Subclass(提炼子类)
- Extract Superclass(提炼超类)
- Extract Interface(提炼接口)
- Collapse Hierarchy(折叠继承关系)
- Form Template Method(塑造模板函数)
- Replace Inheritance with Delegation(以委托取代继承)
- Replace Delegation with Inheritance(以继承取代委托)
- 章节十二 大型重构
- 这场游戏的本质
- Tease Apart Inheritance(梳理并分解继承体系)
- Convert Procedural Design to Objects(将过程化设计转化为对象设计)
- Separate Domain from Presentation(将领域和表述/显示分离)
- Extract Hierarchy(提炼继承体系)
- 章节十三 重构,复用与现实
- 现实的检验
- 为什么开发者不愿意重构他们的程序?
- 现实的检验(再论)
- 重构的资源和参考资料
- 从重构联想到软件复用和技术传播
- 结语
- 参考文献
- 章节十四 重构工具
- 使用工具进行重构
- 重构工具的技术标准(Technical Criteria )
- 重构工具的实用标准(Practical Criteria )
- 小结
- 章节十五 集成
- 参考书目