在開始前的最後一刻,還需要提醒的是,也許你帶著滿腔熱情,想把代碼重構得優雅極致,但就像平時的工作一樣,重構的過程更需要的是理性思考,而不是沖動。每一次重構實踐,都應該包含了對設計、實現、可維護、可擴展性,以及成本的估算和權衡。
所以,首先看看對於Statement方法來說,從哪裡入手比較合適?
[csharp]
public string Statement()
{
double totalAmount = 0;
int frequentRenterPoints = 0;
string result = "Rental Record for " + Name + "\n";
foreach (Rental rental in Rentals)
{
double thisAmount = 0;
// determine amounts for each line
switch (rental.Movie.PriceCode)
{
case Movie.REGULAR:
thisAmount += 2;
if (rental.DaysRented > 2)
thisAmount += (rental.DaysRented - 2) * 1.5;
break;
case Movie.NEW_RELEASE:
thisAmount += rental.DaysRented * 3;
break;
case Movie.CHILDRENS:
thisAmount += 1.5;
if (rental.DaysRented > 3)
thisAmount += (rental.DaysRented - 3) * 1.5;
break;
}
// add frequent renter points
frequentRenterPoints++;
// add bonus for a two day new release rental
if (rental.Movie.PriceCode == Movie.NEW_RELEASE &&
rental.DaysRented > 1) frequentRenterPoints++;
// show figures for this rental
result += "\t" + rental.Movie.Title + "\t" + thisAmount.ToString() + "\n";
totalAmount += thisAmount;
}
// add footer lines
result += "Amount owed is " + totalAmount.ToString() + "\n";
result += "You earned " + frequentRenterPoints.ToString() + " frequent renter points";
return result;
}
實際上,應該換一個問題:對現在的程序來說,哪裡最容易產生變化?畢竟,如果代碼不會變化,重構就是多余的行為。優雅的代碼只能滿足程序員的審美需求,而不是客戶對於功能的需求。
《重構》中指出:該程序最可能產生的變化有三點:
1.報告輸出的類型可能變化,例如由普通字符串變成HTML格式的文本
2.計費方式可能發生變化
3.影片類型可能發生變化
無論哪一種變化,上面的Statement方法都不能很好地應對——它太胖了,涉及的邏輯、細節太多。所以,第一步,《重構》的作者選擇將該方法中最長、並且同時涉及上述所有變化的,計算每一部影片花費的那個switch給抽取出去。
具體步驟是:
1.在Customer中新建一個計算花費的新方法AmountFor:
[csharp]
public int AmountFor(Rental rental)
{
return 0;
}
2.把switch的代碼copy(不是剪切)到AmountFor裡:
[csharp]
public int AmountFor(Rental rental)
{
switch (rental.Movie.PriceCode)
{
case Movie.REGULAR:
thisAmount += 2;
if (rental.DaysRented > 2)
thisAmount += (rental.DaysRented - 2) * 1.5;
break;
case Movie.NEW_RELEASE:
thisAmount += rental.DaysRented * 3;
break;
case Movie.CHILDRENS:
thisAmount += 1.5;
if (rental.DaysRented > 3)
thisAmount += (rental.DaysRented - 3) * 1.5;
break;
}
return 0;
}
3.但此時的代碼是編譯不了得,因為AmountFor中,thisAmount不存在。所以,在switch前面添加一個thisAmount的聲明:
[csharp]
public int AmountFor(Rental rental)
{
int thisAmount = 0;
switch (rental.Movie.PriceCode)
{
case Movie.REGULAR:
thisAmount += 2;
if (rental.DaysRented > 2)
thisAmount += (rental.DaysRented - 2) * 1.5;
break;
case Movie.NEW_RELEASE:
thisAmount += rental.DaysRented * 3;
break;
case Movie.CHILDRENS:
thisAmount += 1.5;
if (rental.DaysRented > 3)
thisAmount += (rental.DaysRented - 3) * 1.5;
break;
}
return thisAmount;
}
4.此時,代碼還是編譯不了,因為花費計算時用到了小數,而thisAmount是整數。加上強制轉換後:
[csharp]
public int AmountFor(Rental rental)
{
int thisAmount = 0;
switch (rental.Movie.PriceCode)
{
case Movie.REGULAR:
thisAmount += 2;
if (rental.DaysRented > 2)
thisAmount += (int)((rental.DaysRented - 2) * 1.5);
break;
case Movie.NEW_RELEASE:
thisAmount += rental.DaysRented * 3;
break;
case Movie.CHILDRENS:
thisAmount += (int)1.5;
if (rental.DaysRented > 3)
thisAmount += (int)((rental.DaysRented - 3) * 1.5);
break;
}
return thisAmount;
}
5.在認為新的方法完成後,去掉Statement中的switch代碼塊,變成:
[csharp]
public string Statement()
{
double totalAmount = 0;
int frequentRenterPoints = 0;
string result = "Rental Record for " + Name + "\n";
foreach (Rental rental in Rentals)
{
double thisAmount = AmountFor(rental);
// add frequent renter points
frequentRenterPoints++;
// add bonus for a two day new release rental
if (rental.Movie.PriceCode == Movie.NEW_RELEASE &&
rental.DaysRented > 1) frequentRenterPoints++;
// show figures for this rental
result += "\t" + rental.Movie.Title + "\t" + thisAmount.ToString() + "\n";
totalAmount += thisAmount;
}
// add footer lines
result += "Amount owed is " + totalAmount.ToString() + "\n";
result += "You earned " + frequentRenterPoints.ToString() + " frequent renter points";
return result;
}
6.接著是非常重要的一步,運行上一篇中的單元測試項目Tests
7.意外發現,所有測試都失敗了,查看StatementForCharles的錯誤消息如下:
[plain]
Assert.AreEqual 失敗。應為: <
Rental Record for Charles
BraveHeart 12
GodFather 6.5
Amount owed is 18.5
You earned 3 frequent renter points
>,實際為: <
Rental Record for Charles
BraveHeart 12
GodFather 6
Amount owed is 18
You earned 3 frequent renter points>。
8.結果中發現,所有數值都是整數,而期待結果中包含小數。想起剛才我們為了讓編譯通過而添加的強制轉型動作,可以據此合理推出問題就出在AmountFor的返回值類型上。遂更改AmountFor如下:
[csharp]
public double AmountFor(Rental rental)
{
double thisAmount = 0;
switch (rental.Movie.PriceCode)
{
case Movie.REGULAR:
thisAmount += 2;
if (rental.DaysRented > 2)
thisAmount += (rental.DaysRented - 2) * 1.5;
break;
case Movie.NEW_RELEASE:
thisAmount += rental.DaysRented * 3;
break;
case Movie.CHILDRENS:
thisAmount += 1.5;
if (rental.DaysRented > 3)
thisAmount += (rental.DaysRented - 3) * 1.5;
break;
}
return thisAmount;
}
9.運行單元測試項目,結果如下:
至此,一個簡單的方法抽取(Extract Method)重構實踐方算是完成了,總結的步驟如下:
1.思考現有代碼的缺陷、確定重構的價值 www.2cto.com
2.從重構價值入手,確定重構入手點
3.在修改原方法前,先建立一個方法,該方法名稱必須能表達被抽取內容的語義(概念)
4.將待抽取內容復制到新方法中,並根據需要添加局部變量的定義
5.去除原方法中的相關內容,替換成新方法的調用
6.運行單元測試
7.如果測試不通過,檢查測試結果,或調試源代碼,直到測試通過為止
8.重構完成
這個例子包含了作者刻意添加的一個小意外,主要是為了演示測試的重要性。但即使不考慮這種意外,上面的步驟還是十分繁復的,對於重構新手而言,一步一步跟著做可能收獲會更大。除非對重構技術十分熟悉,否則不要輕易跳過上面的任何一步。一來,良好的基礎和習慣是十分重要的。二來,每一步都有它的考慮,跳過任何一步意味著少了一份思考。
作者:virtualxmars