一、重命名AmountFor的局部變量
首先看看我們先前抽取出來的AmountFor方法:
[csharp] view plaincopy
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;
}
方法中有個局部變量:thisAmount,實際上,該名稱是在Statement方法中起的。而對於AmountFor方法而言,只可能有一個amount,對應傳參進來的rental。所以,加上this的修飾反而是畫蛇添足的做法。而我也不太認同《重構》中對該局部變量的新命名(result),result一詞過於寬泛,也有許多其它數據提到過這個問題,例如避免使用如result,item,manager,這一類“萬能”詞匯。所以,改進後的代碼如下:
[csharp] view plaincopy
private double AmountFor(Rental rental)
{
double amount = 0;
switch (rental.Movie.PriceCode)
{
case Movie.REGULAR:
amount += 2;
if (rental.DaysRented > 2)
amount += (rental.DaysRented - 2) * 1.5;
break;
case Movie.NEW_RELEASE:
amount += rental.DaysRented * 3;
break;
case Movie.CHILDRENS:
amount += 1.5;
if (rental.DaysRented > 3)
amount += (rental.DaysRented - 3) * 1.5;
break;
}
return amount;
}
如書上所言,不要忽視這種微小的重構,它的確能降低代碼的理解難度。試想,如果一個朋友本想和你說“聖誕快樂”,卻說成了“新年幸福”,這也許在西方國家並不存在太大問題,但是中國人卻是難以將兩件事情聯系在一起的。所以,讓你的命名確切地表示它的意思,這很重要。在我的編碼工作中,甚至可能超過15%的時間是花在為類、方法、字段等內容的命名上,浪費嗎?一點都不!
二、將AmountFor方法轉移到Rental類
《重構》:目前的AmountFor沒有使用任何Customer類的屬性、字段、方法,它在不在Customer方法中其實並不是那麼重要,而這正是暗示該方法不應該屬於該類型的明確信號,Rental類才是它的歸屬地。下面是重構的過程:
1.在Rental類中創建一個Charge屬性,並將AmountFor中的代碼抄過去:
[csharp]
public double Charge
{
get
{
double amount = 0;
switch (Movie.PriceCode)
{
case Movie.REGULAR:
amount += 2;
if (DaysRented > 2)
amount += (DaysRented - 2) * 1.5;
break;
case Movie.NEW_RELEASE:
amount += DaysRented * 3;
break;
case Movie.CHILDRENS:
amount += 1.5;
if (DaysRented > 3)
amount += (DaysRented - 3) * 1.5;
break;
}
return amount;
}
}
2.把原Customer的AmountFor方法體清空,但仍保留聲明和空的函數體。
[csharp]
private double AmountFor(Rental rental)
{
return 0;
}
3.更改Customer的AmountFor實現為:
[csharp]
private double AmountFor(Rental rental)
{
return rental.Charge;
}
4.運行測試項目,如果不通過,則檢查、修改代碼。直到通過為止
5.修改Statement方法,使其不調用自身類的AmountFor方法來取得Rental的Amount,而是直接問Rental要:
[csharp]
public string Statement()
{
double totalAmount = 0;
int frequentRenterPoints = 0;
string result = "Rental Record for " + Name + "\n";
foreach (Rental rental in Rentals)
{
double thisAmount = rental.Charge;
// 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.運行測試,如果不通過則檢查、修改代碼,直到通過為止。
7.重構完成
需要注意的是,將AmountFor方法的內容遷移到Rental中時,並沒有沿用原來的名字,而是采用了Charge這個名稱,主要原因是對於Customer來說,該數值是Rental的Amount,而對於Rental自己來說,用Charge來稱呼它更貼切。這是一個描述角度的問題,也是很多程序員容易忽略的事情。
三、去除thisAmount局部變量
哪怕只是一個稍微復雜的算法,通常都包含了許多運算中間量/輔助量。當整個運算過程都堆在一個方法中時,這些繁雜的變量就交織在一起。如果少一個中間量該算法可以正常運行,為什麼不把它去掉呢?許多程序員、數學家都有這種追求極致的癖好不是麼?實際上也不能說是癖好,應該說少一個變量,意味著該算法更簡單,而越簡單的算法能為這些人帶來越多的成就感,甚至是榮譽感。拋開精神上的收益不談,少一個變量確實會讓該方法更容易維護。對於Statement方法而言,每一個循環中,thisAmount局部變量只被賦值了一次,而使用了兩次,該初始值為rental.Charge。所以,自然地,所有使用到thisAmount的地方換成rental.Charge屬性的調用應該是等價的:
[csharp]
public string Statement()
{
double totalAmount = 0;
int frequentRenterPoints = 0;
string result = "Rental Record for " + Name + "\n";
foreach (Rental rental in Rentals)
{
// 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" + rental.Charge.ToString() + "\n";
totalAmount += rental.Charge;
}
// add footer lines
result += "Amount owed is " + totalAmount.ToString() + "\n";
result += "You earned " + frequentRenterPoints.ToString() + " frequent renter points";
return result;
}
拋開書上提到可能存在的效率問題,我更願意從語義的角度去理解這種重構。上面總共使用了rental.Charge兩次,可以分別描述為:
1.給我一個rental對象的Charge值的文本,我要將它和影片的標題連成一個行表示該影片花費的描述文本。
2.被我一個rental對象的Charge值,我要把它累加到總花費中。
兩次描述中,都直接使用了“rental對象的Charge值”這樣的概念,而不是“給我一個變量,該變量保存了一個rental對象的Charge值”,後面這種是程序員的典型思維方式。它無助於理解算法的本質,而是更容易使查看代碼的人陷入算法的實現細節。所以,當你需要用一個概念時,直接用它,不要繞彎子。當然,真正的問題在於你能很好地總結出這種概念——重構就有這樣的效果。
注:別忘了在重構後運行單元測試
四、將FrequentRenterPoint的計算抽取、遷移到Rental類中
在精簡後的Statement方法中,可以清楚地看到,它還包含了求每一個Rental對象的FrequentRenterPoint的計算。這和求Rental的Charge幾乎是一致的。所以,可以使用同樣的方法來處理它:
1.在Rental類型中,創建新的屬性FrequentRenterPoints,並將Statement方法中的計算邏輯抄到該屬性中,注意要去掉rental對象的引用,因為它現在就屬於Rental類:
[csharp]
public int FrequentRenterPoints
{
get
{
int frequentRenterPoints = 0;
// add frequent renter points
frequentRenterPoints++;
// add bonus for a two day new release rental
if (Movie.PriceCode == Movie.NEW_RELEASE &&
DaysRented > 1) frequentRenterPoints++;
return frequentRenterPoints;
}
}
2.運行單元測試,如果不通過則檢查、調試、修改,直到通過為止
3.該屬性的實現非常“笨”,實際想表達的就是如果符合條件則得2分,否則得1分,所以可重構為:
[csharp]
public int FrequentRenterPoints
{
get
{
if (Movie.PriceCode == Movie.NEW_RELEASE && DaysRented > 1)
{
return 2;
}
else
{
return 1;
}
}
}
可以使用更簡單的三木操作符 :? 來替代if-else,但通常後者更容易被理解。
4.運行單元測試,如果不通過則檢查、調試、修改,直到通過為止
5.替換原Statement中關於frequentRenterPoints的計算,使用Rental的FrequentRentalPoints屬性來進行計算:
[csharp]
public string Statement()
{
double totalAmount = 0;
int frequentRenterPoints = 0;
string result = "Rental Record for " + Name + "\n";
foreach (Rental rental in Rentals)
{
frequentRenterPoints += rental.FrequentRenterPoints;
// show figures for this rental
result += "\t" + rental.Movie.Title + "\t" + rental.Charge.ToString() + "\n";
totalAmount += rental.Charge;
}
// add footer lines
result += "Amount owed is " + totalAmount.ToString() + "\n";
result += "You earned " + frequentRenterPoints.ToString() + " frequent renter points";
return result;
}
6.運行單元測試,如果不通過則檢查、調試、修改,直到通過為止
7.重構完成
五、抽取花費總值的計算以及總積分的計算
現在已經較容易看出,Statement方法中,除了報告文本的生成外,主要還做了兩樣事情:計算總花費,計算總積分,而現在它們是混合在一起的。而我們的確可以把它們抽取為兩個獨立的查詢過程(對C#而言可以是兩個屬性),分別命名為TotalCharge、TotalRentalPoints:
1.在Customer類中添加一個屬性TotalCharge如下:
[csharp]
private double TotalCharge
{
get
{
double sum = 0;
foreach (Rental aRental in Rentals)
{
sum += aRental.Charge;
}
return sum;
}
}
2.將Statement方法中所有關於總花費的局部變量、過程都去掉,換成對TotalCharge的調用:
[csharp]
public string Statement()
{
int frequentRenterPoints = 0;
string result = "Rental Record for " + Name + "\n";
foreach (Rental rental in Rentals)
{
frequentRenterPoints += rental.FrequentRenterPoints;
// show figures for this rental
result += "\t" + rental.Movie.Title + "\t" + rental.Charge.ToString() + "\n";
}
// add footer lines
result += "Amount owed is " + TotalCharge.ToString() + "\n";
result += "You earned " + frequentRenterPoints.ToString() + " frequent renter points";
return result;
}
3.運行單元測試,如果不通過則檢查、調試、修改,直到通過為止
4.在Customer類中添加TotalRenterPoints屬性如下:
[csharp]
private int TotalRenterPoints
{
get
{
int points = 0;
foreach (Rental aRental in Rentals)
{
points += aRental.FrequentRenterPoints;
}
return points;
}
}
5.將Statement方法中所有關於總積分的局部變量、過程都去掉,換成對TotalRenterPoints的調用:
[csharp]
public string Statement()
{
string result = "Rental Record for " + Name + "\n";
foreach (Rental rental in Rentals)
{
// show figures for this rental
result += "\t" + rental.Movie.Title + "\t" + rental.Charge.ToString() + "\n";
}
// add footer lines
result += "Amount owed is " + TotalCharge.ToString() + "\n";
result += "You earned " + TotalRenterPoints.ToString() + " frequent renter points";
return result;
}
6.運行單元測試,如果不通過則檢查、調試、修改,直到通過為止
7.重構完成 www.2cto.com
至此,我們重新審視一下Statement方法,比起最初的版本而言,哪一個更容易看出它所包含的邏輯?最起碼我掃一眼後面這個版本就知道它的工作如下:
總的來說,它生成了一份報告文本;這個報告用用戶名生成了一報告頭;為每一個Rental生成了一份標題+花費的記錄;報告的Footer包含兩樣內容,分別是該客戶的總花費,以及他能得到的總積分是多少。
至於很多人會質疑,抽取的TotalCharge和TotalRenterPoints會帶來兩次額外的Rentals遍歷,以至於存在效率降低的的問題。還是參閱《重構》書中的內容吧。
作者:virtualxmars