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如下圖