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

《重構》C#版實現(三)Statement的整體重構

編輯:C#入門知識

一、重命名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

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