### 分解并重組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),下一件事就是去掉舊函數。編譯器會告訴我是否我漏掉了什么。然后我進行測試,看看有沒有破壞什么東西。

圖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〕總結剛才所做的修改。和先前一樣,左頁是修改前的圖,右頁是修改后的圖。

圖1.4 [常客積點計算」函數被提煉及搬移之前的class diagrams

圖1.5 「常客積點計算」函數被提煉及搬移之前的sequence diagrams

圖1.6 「常客積點計算」函數被提煉及搬移之后的class diagrams

圖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() 重構前后的變化。

圖1.8 「總量計算」函數被提煉前的class diagram

圖1.9 「總量計算」函數被提煉前的sequence diagram

圖1.10 「總量計算」函數被提煉后的class diagram

圖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 )
- 小結
- 章節十五 集成
- 參考書目