程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#入門知識 >> 《重構》C#版實現(四)switch的多態化處理

《重構》C#版實現(四)switch的多態化處理

編輯:C#入門知識

首先,列出到現在為止,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

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved