首先,列出到現在為止,Movie類和Rental類的代碼如下:
[csharp]
public class Movie
{
public const int CHILDRENS = 2;
public const int REGULAR = 0;
public const int NEW_RELEASE = 1;
public string Title { get; private set; }
public int PriceCode { get; private set; }
public Movie(string title, int priceCode)
{
Title = title;
PriceCode = priceCode;
}
}
public class Rental
{
public Movie Movie { get; private set; }
public int DaysRented { get; private set; }
public double Charge
{
get
{
double result = 0;
switch (Movie.PriceCode)
{
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;
}
}
public int FrequentRenterPoints
{
get
{
if (Movie.PriceCode == Movie.NEW_RELEASE && DaysRented > 1)
{
return 2;
}
else
{
return 1;
}
}
}
public Rental(Movie rented, int days)
{
Movie = rented;
DaysRented = days;
}
}
一、為價格計算尋找更合適的“家”
《重構》中提到一個考慮:該程序的一個主要變化點在於:影片類型很可能增加或發生變化。而目前來說,跟影片類型先關的代碼分散在上面兩個類中。在另一本書《代碼整潔之道》中,提到一個原則:如果某些代碼的修改頻度一致,則應該把它們盡可能放在一起。放在我們這兒,意味著,如果我們在Movie類中增加了影片類型,意味著也要去修改Rental類的Charge屬性,這種修改涉及到了兩個類。假設情景是在大得多的項目中,則有可能是要修改兩個不同的文件,甚至是不同的項目或程序集。而後面的這些情景通常會產生大量的BUG。而如果將修改頻度一致的內容放在一起(同一個文件,甚至是同一個類中),則內容的同步也會變得更容易,BUG產生的幾率會更低。
綜上所述,我們需要把Rental關於Charge的計算過程遷移到Movie類中——雖然會帶來多一層間接性,以換取程序的高可維護、可擴展性:
1.在Movie類中創建一個ChargeFor方法,並將Rental.Charge屬性的代碼抄過去:
[csharp]
public double ChargeFor(int daysRented)
{
double result = 0;
switch (PriceCode)
{
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;
}
注意,在代碼遷移時,可能存在一些問題,諸如原本PriceCode是使用Rental的Movie對象來調用的,而現在因為它成為了自身的屬性,所以也就沒有必要使用Movie對象了。
2.修改Rental.Charge屬性,讓它通過委托給Movie的新方法來完成計算:
[csharp]
public double Charge
{
get
{
return Movie.ChargeFor(DaysRented);
}
}
3.運行單元測試,如果不通過則調試、修改直到通過為止
4.重構完成
二、用多態來封裝價格算法
其實,就《重構》中所列出的這份代碼而言,個人認為不需要更多的優化了。在整個程序中只有一個地方需要對PriceCode進行分派處理,這樣的集中也是一種不錯的選擇——畢竟需要調整價格計算算法時,我們一定會直接定位到該ChargeFor方法。而什麼樣的信號會明確地告訴我們需要把switch轉換成多態呢?我想應該是在代碼中出現兩處以上對PriceCode的switch分派處理時,這時,多態化的信號就很明確了。而《重構》更多的是想以此為例,演示如何用多態的技術來解決類似的問題。
為了要引入多態,就開始需要介入設計模式了。有很多人可能不了解它,甚至會因此產生抗拒的心態。其實完全沒必要,設計模式說白了都很簡單,尤其是它們的實現。最難的只是要活學活用它們的適用情境。
1.創建一個Price接口,用來表示影片的可計算概念
[csharp]
public interface Price
{
double ChargeFor(int daysRented);
}
2.創建一個實現了Price接口的Regular類,實現對REGULAR類型影片的價格計算。重新審視關於該類型影片價格的計算方法,可以得到下面代碼:
[csharp]
public sealed class RegularPrice : Price
{
public double ChargeFor(int daysRented)
{
if (daysRented > 2)
{
return 2 + (daysRented - 2) * 1.5;
}
else
{
return 2;
}
}
}
3.修改Movie.ChargeFor中,關於REGULAR影片價格計算的片段,如下
[csharp]
public double ChargeFor(int daysRented)
{
double result = 0;
switch (PriceCode)
{
case Movie.REGULAR:
result = (new RegularPrice()).ChargeFor(daysRented);
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;
}
4.運行單元測試,如果不通過則調試、修改直到通過為止
5.用同樣的方式處理NEW_RELEASE和CHILDRENS兩種價格算法,得到代碼如下:
兩個計算價格的新類型:
[csharp]
public sealed class NewReleasePrice : Price
{
public double ChargeFor(int daysRented)
{
return daysRented * 3;
}
}
public sealed class ChildrenPrice : Price
{
public double ChargeFor(int daysRented)
{
if (daysRented > 3)
{
return 1.5 + (daysRented - 3) * 1.5;
}
else
{
return 1.5;
}
}
}
修改後的Movie.ChargeFor方法:
[csharp]
public double ChargeFor(int daysRented)
{
double result = 0;
switch (PriceCode)
{
case Movie.REGULAR:
result = (new RegularPrice()).ChargeFor(daysRented);
break;
case Movie.NEW_RELEASE:
result = (new NewReleasePrice()).ChargeFor(daysRented);
break;
case Movie.CHILDRENS:
result = (new ChildrenPrice()).ChargeFor(daysRented);
break;
}
return result;
}
6.運行單元測試,如果不通過則調試、修改直到通過為止
7.注意到,每一個case子句中,做的事情實際上是兩部分:創建對應的價格計算策略對象;用該對象計算價格。後面這一步對於三個case來說是沒有區別的,所以可以進一步重構成:
[csharp]
public double ChargeFor(int daysRented)
{
Price price = null;
switch (PriceCode)
{
case Movie.REGULAR:
price = new RegularPrice();
break;
case Movie.NEW_RELEASE:
price = new NewReleasePrice();
break;
case Movie.CHILDRENS:
price = new ChildrenPrice();
break;
}
return price.ChargeFor(daysRented);
}
8.行單元測試,如果不通過則調試、修改直到通過為止
9.基於一個事實:Movie對象的PriceCode屬性僅在創建時被賦值了一次,並且僅此一次。所以如果每次調用ChargeFor時都需要判斷一次PriceCode是不合理的。應該將這種判斷遷移到設置PriceCode的那個地方去——構造函數。由此而來的問題是,必須為Movie添加一個Price的字段,以便ChargeFor知道構造函數對計算方法的選擇,並用它來進行計算。
[csharp]
private Price price; // 新添加的價格計算對象
public Movie(string title, int priceCode)
{
Title = title;
PriceCode = priceCode;
Price price = null;
switch (PriceCode)
{
case Movie.REGULAR:
price = new RegularPrice();
break;
case Movie.NEW_RELEASE:
price = new NewReleasePrice();
break;
case Movie.CHILDRENS:
price = new ChildrenPrice();
break;
}
}
public double ChargeFor(int daysRented)
{
return price.ChargeFor(daysRented);
}
10.行單元測試,如果不通過則調試、修改直到通過為止
11.重構完成
三、用多態來封裝積分算法
積分的計算過程和價格計算沒有太大的區別,可以進行類似的重構來進行多態化封裝:
1.在Movie方法中添加FrequentRenterPointsFor方法,並將Rental的FrequentRenterPoints屬性中的內容超過去:
[csharp]
public int FrequentRenterPointsFor(int daysRented)
{
if (PriceCode == Movie.NEW_RELEASE && daysRented > 1)
{
return 2;
}
else
{
return 1;
}
}
2.修改Rental的FrequentRenterPoints屬性的實現,通過委托Movie對象的FrequentRenterPointsFor方法來實現:
[csharp]
public int FrequentRenterPoints
{
get
{
return Movie.FrequentRenterPointsFor(DaysRented);
}
}
3.行單元測試,如果不通過則調試、修改直到通過為止
4.為Price接口添加一個FrequentRenterPointsFor方法:
[csharp]
public interface Price
{
double ChargeFor(int daysRented);
int FrequentRenterPointsFor(int daysRented);
}
5.分別在RegularPrice、NewReleasePrice、ChildrenPrice中分別實現該接口方法:
[csharp]
public sealed class RegularPrice : Price
{
public double ChargeFor(int daysRented)
{
if (daysRented > 2)
{
return 2 + (daysRented - 2) * 1.5;
}
else
{
return 2;
}
}
public int FrequentRenterPointsFor(int daysRented)
{
return 1;
}
}
public sealed class NewReleasePrice : Price
{
public double ChargeFor(int daysRented)
{
return daysRented * 3;
}
public int FrequentRenterPointsFor(int daysRented)
{
return daysRented > 1 ? 2 : 1;
}
}
public sealed class ChildrenPrice : Price
{
public double ChargeFor(int daysRented)
{
if (daysRented > 3)
{
return 1.5 + (daysRented - 3) * 1.5;
}
else
{
return 1.5;
}
}
public int FrequentRenterPointsFor(int daysRented)
{
return 1;
}
}
6.修改Movie.FrequentPointsFor方法的實現,使它通過委托給Price對象來完成計算:
[csharp]
public int FrequentRenterPointsFor(int daysRented)
{
return price.FrequentRenterPointsFor(daysRented);
}
7.行單元測試,如果不通過則調試、修改直到通過為止
8.重構完成
四、徹底消除switch代碼
上面的重構產生了一個新的問題,即對於價格來說,出現了兩個相關概念:PriceCode和Price,它們分飾類型和計算二職。這是非常不好的現象,意味著如果將來更改了二者任何其中的一個,必須同時改變另一個。《重構》上的解決方案是在同步它們的修改,即將price的創建放在PriceCode的設置方法中(C#中對應的是PriceCode屬性的set方法)。而我覺得這種做法也不是盡善盡美的。根本上,這裡應該徹底消除switch代碼的使用。
很多同學都對此有疑惑,switch代碼怎麼可能消除呢?畢竟,最起碼也要判斷一次PriceCode,然後才能計算價格的呀。我想,這些同學過於陷入他們為自己所營造的困局中了——將判斷限定於if-else又或者是switch中。實際上,回想一下我們在第(一)篇中添加的參考數據生成的那段代碼:
[csharp]
Movie braveHeart = new Movie("BraveHeart", Movie.NEW_RELEASE);
Movie godFather = new Movie("GodFather", Movie.REGULAR);
Movie wallE = new Movie("Wall-E", Movie.CHILDRENS);
這段代碼應該被稱為客戶代碼,它是Movie的客戶。而在它創建Movie對象時,實際上已經做出了第一次判斷、選擇——決定到底使用哪一種影片類型,進而又確定了應該價格的計算方式。我們又何苦再把事情重復一次呢。所以,代碼進行下述重構:
1.將Movie的NEW_RELEASE、REGULAR、CHILDRENS常量重新定義如下:
[csharp]
public static readonly Price CHILDRENS = new ChildrenPrice();
public static readonly Price REGULAR = new RegularPrice();
public static readonly Price NEW_RELEASE = new NewReleasePrice();
2.將Movie的構造函數修改如下:
[csharp]
public Movie(string title, Price priceCode)
{
Title = title;
price = priceCode;
}
3.行單元測試,如果不通過則調試、修改直到通過為止
4.重構完成
五、善後
至此,我們已完全去除了對switch的依賴。最美好的事情不在這,而在於,客戶代碼不需要任何改變——這正是很多同學在重構的過程中所擔心的主要問題。但我們忘記了處理一個問題:Movie的PriceCode屬性被孤立了,它的值現在沒有任何人使用,也沒有任何意義。一個問題是:它的存在是否有價值,是否應該移除該屬性。設個問題的確定涉及到客戶代碼的編寫是否依賴於該屬性。我們不作任何假設,為了盡可能保證現有客戶代碼的正確性,需要做一些善後的工作:
1.為Price接口類添加Code屬性如下:
[csharp]
public interface Price
{
int Code { get; }
double ChargeFor(int daysRented);
int FrequentRenterPointsFor(int daysRented);
}
2.別在RegularPrice、NewReleasePrice、ChildrenPrice中分別實現該接口屬性:
[csharp] view plaincopy
public sealed class RegularPrice : Price
{
public int Code { get { return 0; } }
// ... 其余代碼
}
public sealed class NewReleasePrice : Price
{
public int Code { get { return 1; } }
<pre name="code" class="csharp"> // ... 其余代碼</pre>}public sealed class ChildrenPrice : Price{public int Code { get { return 2; } }<pre name="code" class="csharp"> // ... 其余代碼</pre>}<p></p>
<pre></pre>
3.修改Movie.PriceCode的屬性實現:<br>
<pre name="code" class="csharp">public int PriceCode
{
get { return price.Code; }
}</pre>4.運行單元測試,如果不通過則調試、修改直到通過為止
<p></p>
<p>8.重構完成</p>
<p><strong><span style="font-size:16px">六、小結</span></strong><br>
至此,MovieRentalHouse的整個重構告一段落了。雖然還有些地方可以重構,例如Statement方法還可以通過Extract Method來概念化,但作為訓練教程,做到這裡足矣。該例子實際上並不復雜,也沒有必要進行這麼復雜的重構。而這麼做的主要目的是為了演示重構方法的價值。倘若將這些代碼放到更復雜的交互環境下,很可能重構就是值得的。重點在於,是否需要重構,完全取決於我們對代碼質量的評價,以及它是否需要可擴展性、可維護性的判斷。<br>
最後,總結一下重構過程中最容易被忽略,也是最值得反復強調的問題如下:<br>
1.了解現有代碼的問題,確定代碼的重構價值<br>
2.根據重構價值確定重構入手點<br>
3.將處理過程語義(概念)化<br>
4.不要過早介入性能優化這個主題<br>
5.針對變化點重構,而不是興趣或沖動<br>
6.重構需要自動測試的保障<br>
7.不要過度重構</p>
作者:virtualxmars