本文用eclipse的自動重構功能對一個程式執行個體進行重構,目的是探索Eclipse自動重構可以在多大程度上輔助重構這個過程。程式執行個體使用《Refactoring:Improving the Design of Existing Code》一書中的例子。
Eclipse的自動重構功能能夠很好地支援各種程式元素的重新命名,並自動更新相關的引用。Eclipse能夠支援方法、欄位在類之間移動,並自動更新引用。Eclipse較好地支援內聯欄位、函數的更新替換。Eclipse較好地支援抽取方法、變數等程式元素。
重構的過程是一個不斷嘗試和探索的過程。Eclipse的重構支援撤銷和重做,並且能夠預覽重構結果,這些是很實用的功能。
Eclipse的重新命名、抽取方法、移動、內聯功能、更改方法特徵符等代碼結構層級的重構方法,是比較成熟同時也值得使用的功能。至於設計結構上的重構,eclipse還不能很好地支援。但是作者相信,自動重構的理念應該是"工具輔助下的重構工作",人仍然承擔大部分重構工作。
一、預備工作
本文使用《Refactoring:Improving the Design of Existing Code》一書第一章的例子。重構前的代碼及每一步重構後的代碼見附件。讀者最好配合《Refactoring:Improving the Design of Existing Code》一書閱讀本文。
Eclipse使用如下版本:
同時安裝了中文語言套件。
二、重構第一步:分解並重組statement()
目的:
1、 把statement()函數中的swich語句提煉到獨立的函數amountFor()中。
2、 修改amountFor()參數命名
重構方法:
Extract Method
Rename Method
方法:
1、選中swich語句的代碼塊,在右鍵菜單中選擇"重構/抽取方法",出現參數對話方塊。Eclipse自動分析代碼塊中的局部變數,找到了兩個局部變數:each和thisAmount。其中,each只是在代碼塊中被讀取,但thisAmount會在代碼塊中被修改。按照重構Extract Method總結出來的規則,應該把each當作抽取函數的參數、thisAmount當作抽取函數的傳回值。然而Eclipse並不做區分,直接把這兩個變數當作抽取新方法的參數,。
我們的目的是把在抽取函數中不會被修改的each作為參數;會被修改的thisAmount作為傳回值。解決的辦法是,把 double thisAmount = 0; 這行代碼移到switch語句的上面,變成這樣:
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;
}
選中這段代碼,在右鍵菜單中選擇"重構/抽取方法",eclipse這次變得聰明點了,。
選擇"預覽"按鈕預先查看重構後的結果,符合我們最初的目的。
選擇"確定"按鈕,重構後的代碼片斷如下:
public String statement() {
double totalAmount = 0;
int frequentRenterPoints = 0;
Enumeration rentals = _rentals.elements();
String result = "Rental Record for " + getName() + " ";
while(rentals.hasMoreElements()){
Rental each = (Rental)rentals.nextElement();
double thisAmount = amountFor(each);
frequentRenterPoints ++;
if((each.getMovie().getPriceCode())==Movie.NEW_RELEASE &&each.getDaysRented()>1)
frequentRenterPoints ++;
result += " " + each.getMovie().getTitle() + " " +String.valueOf(thisAmount) + " ";
totalAmount += thisAmount;
}
result += "Amount owed is " + String.valueOf(totalAmount) + " ";
result += "You earned " + String.valueOf(frequentRenterPoints) + " frequent renter points";
return result;
}
/**
* @param each
* @return
*/
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;
}
2、選中amountFor()的參數each,在右鍵菜單中選擇"重構/重新命名",在對話方塊中輸入新的名稱:aRental,選擇確定,amountFor()中所有each的引用全部被替換成新的名稱。用同樣的辦法修改amountFor()中的局部變數thisAmount為result。重構後的amountFor()代碼如下:
/**
* @param aRental
* @return
*/
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;
}
三、重構第二步:搬移"金額計算"代碼
目的:
1、 將函數amountFor()轉移到Rental類中,並更名為getCharge()。
2、 更新並替換所有對amountFor()的引用。
重構方法:
Move Method
Change Method signatrue
Inline Method
Inline Temp
方法:
1、選中函數amountFor()的定義,在右鍵菜單中選擇"重構/移動",顯示參數設定對話方塊。把新方法名改成getCharge。按下"確定"按鈕,Customer Class中的amountFor()函數被移動到Rental Class中,並更名為:getCharge()。
同時eclipse自動在Customer的amountFor()函數中添加一行對新函數的"委託"代碼:
private double amountFor(Rental aRental) {
return aRental.getCharge();
}
這行代碼會產生編譯錯誤,原因是amountFor()的private型被傳遞到了新的方法中:
/**
* @param this
* @return
*/
private double getCharge() {
……
}
2、繼續重構!選中getCharge()方法,在右鍵菜單中選擇"重構/更改方法特徵符",彈出參數選擇對話方塊,把存取修飾詞從private改成public。Eclipse的編譯錯誤提示自動消失。
3、回到Customer類,把所有對amountFor()引用的地方替換成直接對getCharge()的引用。選中Customer類的函數amountFor(Rental aRental),在右鍵菜單中選擇"重構/內聯",出現參數選擇對話方塊。
選擇"確認"按鈕,引用amountFor()的地方被替換成對getCharge()的引用。
public String statement() {
……
double thisAmount = each.getCharge();
……
}
4、除去臨時變數thisAmount。
選中變數thisAmount,在右鍵菜單中選擇"重構/內聯",重構預覽視窗如下,可見達到了重構的目的。按下"確認"按鈕重構代碼。
statement()代碼:
public String statement() {
double totalAmount = 0; // 總消費金額
int frequentRenterPoints = 0; // 常客積點
Enumeration rentals = _rentals.elements();
String result = "Rental Record for " + getName() + " ";
while(rentals.hasMoreElements()){
Rental each = (Rental)rentals.nextElement(); //取得一筆租借記錄
// add frequent renter points(累加 常客積點)
frequentRenterPoints ++;
// add bouns for a two day new release rental
if((each.getMovie().getPriceCode())==Movie.NEW_RELEASE && each.getDaysRented()>1)
frequentRenterPoints ++;
// show figures for this rental(顯示此筆租借資料)
result += " " + each.getMovie().getTitle() + " " +
String.valueOf(each.getCharge()) + " ";
totalAmount += each.getCharge();
}
// add footer lines(結尾列印)
result += "Amount owed is " + String.valueOf(totalAmount) + " ";
result += "You earned " + String.valueOf(frequentRenterPoints) + " frequent renter points";
return result;
}
四、重構第三步:提煉"常客積點計算"代碼
目的:提取"常客積點計算"代碼並放在Rental類中,"常客積點計算"代碼如下。
public String statement() {
……
// add frequent renter points
frequentRenterPoints ++;
// add bouns for a two day new release rental
if((each.getMovie().getPriceCode())==Movie.NEW_RELEASE && each.getDaysRented()>1)
frequentRenterPoints ++;
……
}
重構後的代碼如下:
frequentRenterPoints += each.getFrequentRenterPoints();
重構方法:
Extract Method
Move Method
Change Method signatrue
Inline Method
方法:
1、 首先,抽取代碼到獨立的函數中。
用"抽取方法"重構代碼,函數名:getFrequentRenterPoints。很遺憾,eclipse的不能產生諸如:frequentRenterPoints += getFrequentRenterPoints(Rental aRental); 的代碼。原因是執行自增操作的局部變數frequentRenterPoints要出現在等式右邊,因此抽取函數getFrequentRenterPoints()一定要把frequentRenterPoints作為參數。手工修改函數和對函數的引用,重構後的代碼如下:
public String statement() {
……
while(rentals.hasMoreElements()){
……
frequentRenterPoints += getFrequentRenterPoints(each);
……
}
……
}
/**
* @param each
* @return
*/
private int getFrequentRenterPoints(Rental each) {
if((each.getMovie().getPriceCode())==Movie.NEW_RELEASE && each.getDaysRented()>1)
return 2;
else
return 1;
}
2、 把getFrequentRenterPoints()移動到Rental類中。
3、 對getFrequentRenterPoints()"更改方法特徵符"為public。
4、 對Customer的函數getFrequentRenterPoints()執行內聯操作,重構目標完成。
五、重構第四步:去除臨時變數(totalAmount和frequentRenterPoints)
目的:去除臨時變數(totalAmount和frequentRenterPoints)
方法:
1、 分析totalAmount和frequentRenterPoints的定義和引用結構如下:
// 聲明和定義
double totalAmount = 0;
int frequentRenterPoints = 0;
……
// 在迴圈中修改
while(rentals.hasMoreElements()){
……
frequentRenterPoints += each.getFrequentRenterPoints();
……
totalAmount += each.getCharge();
……
}
……
// 在迴圈外使用
result += "Amount owed is " + String.valueOf(totalAmount) + " ";
result += "You earned " + String.valueOf(frequentRenterPoints) + " frequent renter points";
……
上述兩個變數在迴圈體外面定義和使用,在迴圈中被修改,運用Replace Temp with Query方法去除這兩個臨時變數是一項稍微複雜的重構。很遺憾,eclipse目前不支援這樣的重構。
2、手工修改代碼。
六、重構第五步:運用多態取代與價格相關的條件邏輯
目的:
1、 把Rental類中的函數getCharge()移動到Movie類中。
2、 把Rental類中的函數getFrequentRenterPoints()移動到Movie類中。
重構方法:
Move Method
Inline Method
方法:
1、 選中Rental類中的函數getCharge(),右鍵菜單選中"重構/移動",eclipse提示找不到接收者,不能移動。原因在於這行語句:
switch(getMovie().getPriceCode()){//取得影片出租價格
選中getMovie(),右鍵菜單選中"重構/內聯",確定後代碼成為:
switch(_movie.getPriceCode()){ //取得影片出租價格
選中getCharge(),執行"重構/移動"後,函數被移動到Movie類中。然而這隻是部分達成了重構目的,我們發現,移動後的代碼把Rental作為參數傳給了getCharge(),手工修改一下,代碼變成:
class Movie ……
/**
* @param this
* @return
*/
public double getCharge(int _daysRented) {
double result = 0;
switch(getPriceCode()){ //取得影片出租價格
case Movie.REGULAR: // 普通片
result += 2;
if(_daysRented>2)
result += (_daysRented-2)*1.5;
break;
case Movie.NEW_RELEASE: // 新片
result += _daysRented*3;
break;
case Movie.CHILDRENS: // 兒童片
result += 1.5;
if(_daysRented>3)
result += (_daysRented-3)*1.5;
break;
}
return result;
}
class Rental……
/**
* @param this
* @return
*/
public double getCharge() {
return _movie.getCharge(_daysRented);
}
2、用同樣的步驟處理getFrequentRenterPoints(),重構後的代碼:
class Movie ……
/**
* @param frequentRenterPoints
* @param this
* @return
*/
public int getFrequentRenterPoints(int daysRented) {
if((getPriceCode())==Movie.NEW_RELEASE && daysRented>1)
return 2;
else
return 1;
}
class Rental……
/**
* @param frequentRenterPoints
* @param this
* @return
*/
public int getFrequentRenterPoints(int daysRented) {
if((getPriceCode())==Movie.NEW_RELEASE && daysRented>1)
return 2;
else
return 1;
}
七、重構第六步:終於……我們來到繼承
目的:對switch語句引入state模式。
方法:
很遺憾,不得不在這裡提前結束eclipse的自動重構之旅。Eclipse幾乎不能做結構上的重構。也許Martin Fowler在書中呼喚的自動重構工具止於"工具輔助下的重構工作"這一理念。藝術是人類的專利,編程藝術的夢想將持續下去。
感興趣的讀者可以查看手工重構的最後一步代碼。將重構進行到底!
附錄:eclipse支援的重構方法(摘自eclipse中文協助)
名稱功能
撤銷執行上一次重構的"撤銷"。只要除了重構之外尚未執行任何其它源更改,重構撤銷緩衝區就有效。
重做執行上一次撤銷重構的"重做"。只要除了重構之外尚未執行任何其它源更改,重構撤銷/重做緩衝區就有效。
重新命名 啟動"重新命名"重構對話方塊:重新命名所選擇的元素,並更正對元素的所有引用(如果啟用了的話)(還在其它檔案中)。可用於:方法、欄位、局部變數、方法參數、類型、編譯單元、包、源檔案夾和項目,以及解析為這些元素類型中的其中一種的文本選擇部分。
移動 啟動"移動"重構對話方塊:移動所選擇的元素,並更正對元素的所有引用(如果啟用了的話)(還在其它檔案中)。適用於:一個執行個體方法(可以將它移至某個組件)、一個或多個靜態方法、靜態欄位、類型、編譯單元、包、源檔案夾和項目,以及解析為這些元素類型中的其中一種的文本選擇部分。
更改方法特徵符啟動"更改方法特徵符"重構對話方塊。更改參數名稱、參數類型和參數順序,並更新對相應方法的所有引用。此外,可以除去或添加參數,並且可以更改方法傳回型別和它的可視性。可以將此重構應用於方法或解析為方法的文本選擇。
將匿名類轉換為嵌套類啟動"將匿名類轉換為嵌套類"重構對話方塊。協助您將匿名內部類轉換為成員類。可以將此重構應用於匿名內部類。
將巢狀型別轉換成頂層啟動"將巢狀型別轉換為頂層類型"重構對話方塊。為所選成員類型建立新的 Java 編譯單元,並根據需要更新所有引用。對於非靜態成員類型,將添加欄位以允許訪問先前的外圍執行個體。可以將此重構應用於成員類型或解析為成員類型的文本。
下推啟動"下推"重構對話方塊。將一組方法和欄位從一個類移至它的子類。可以將此重構應用於在同一個類型中聲明的一個或多個方法和欄位或者欄位或方法內的文本選擇。
上拉啟動"上拉"重構型中聲明的一個或多個方法、欄位和成員類型,也可以應用於欄位、方法或成員類型內的文本選擇。嚮導。將欄位或方法移至其聲明類的超類或者(對於方法)將方法聲明為超類中的抽象類別。可以將此重構應用於在同一個類
抽取介面啟動"抽取介面"重構對話方塊。使用一組方法建立新介面並使選擇的類實現該介面,並儘可能地將對該類的引用更改為對新介面的引用(可選)。可以將此重構應用於類型。
儘可能使用超類型啟動"儘可能使用超類型"對話方塊。將某個類型的出現替換為它的其中一個超類型,在執行此替換之前,需要標識所有有可能進行此替換的位置。此重構可用於類型。
內聯啟動"內聯"重構對話方塊。內聯局部變數、方法或常量。此重構可用於方法、靜態終態欄位和解析為方法、靜態終態欄位或局部變數的文本選擇。
抽取方法啟動"抽取方法"重構對話方塊。建立一個包含當前所選擇的語句或運算式的新方法,並將選擇替換為對新方法的引用。可以使用編輯菜單中的擴大選擇至以擷取有效選擇範圍。此功能對於清理冗長、雜亂或過於複雜的方法是很有用的。
抽取局部變數啟動"抽取變數"重構對話方塊。建立為當前所選擇的運算式指定的新變數,並將選擇替換為對新變數的引用。此重構可用於解析為局部變數的文本選擇。可以使用編輯菜單中的擴大選擇至以擷取有效選擇範圍。
抽取常量啟動"抽取常量"重構對話方塊。從所選運算式建立靜態終態欄位並替換欄位引用,並且可以選擇重寫同一運算式的其它出現位置。此重構可用於靜態終態欄位和解析為靜態終態欄位的文本選擇。
將局部變數轉換為欄位啟動"將局部變數轉換為欄位"重構對話方塊。將局部變數轉換為欄位。如果該變數是在建立時初始化的,則此操作將把初始化移至新欄位的聲明或類的建構函式。此重構可用於解析為局部變數的文本選擇。
封裝欄位啟動"自封裝欄位"重構對話方塊。將對欄位的所有引用替換為 getting 和 setting 方法。它適用於所選擇的欄位或解析為欄位的文本選擇。