天天看點

代碼重構(六):代碼重構完整案例

無論做什麼事情呢,都要善始善終呢。前邊連續發表了5篇關于重構的部落格,其中分門别類的介紹了一些重構手法。今天的這篇部落格就使用一個完整的示例來總結一下之前的重構規則,也算給之前的關于重構的部落格畫一個句号。今天的示例借鑒于《重構,改善既有代碼的設計》這本書中的第一章的示例,在其基礎上做了一些修改。今天部落格從頭到尾就是一個完整的重構過程。首先會給出需要重構的代碼,然後對其進行分析,然後對症下藥,使用之前我們分享的重構規則對其進行一步步的重構。

先來聊一下該示例的使用場景(如果你有重構這本書的話,可以參加第一章中的示例,不過本部落格中的示例與其有些出入)。就是一個客戶去DVD出租的商店裡進行消費,下方的程式是給店主用的,來根據使用者所借的不同的DVD種類和數量來計算該使用者消費的金額和積分。需求很簡單而且也不難了解。今天部落格會給出原始的代碼,也是需要進行重構的代碼。當然原始代碼完全符合需求,并且可以正确執行。廢話少說,先看示例吧。

一、需要重構的代碼

在本篇部落格的第一部分,我們先給出完成上述需求需要重構的代碼。然後在此基礎上進行分析,使用之前我們提到過的重構手法進行重構。首先我們給出了電影類的實作。在Movie類中有電影的種類(靜态常量):普通電影、兒童電影、新電影,然後有兩個成員變量/常量是priceCode(價格代碼)、title(電影名稱),最後就是我們的構造方法了。該Movie類比較簡單,在此就不做過多的贅述了。

代碼重構(六):代碼重構完整案例
實作完Movie類接下來就是租賃類Rental,這個Rental類的職責就是負責統計某個電影租賃的時間。下方就是這個租賃類,該類也是比較簡單的,其中有兩個字段,一個是租了的電影,另一個就是租賃的時間了。
代碼重構(六):代碼重構完整案例

接下來要實作我們的消費者類了,也就是Customer類。在Customer類中有消費者的名字name和一個數組,該數組中寸的就是租賃電影的集合。其中的statement()方法就是結算該客戶的結算資訊的方法,并将結果進行列印。在此我們需要了解的需求是每種電影的計價方式以及積分的計算規則。

電影價格計算規則:

  普通片兒--2天之内含2天,每部收費2元,超過2天的部分每天收費1.5元

  新片兒--每天每部3元 

  兒童片--3天之内含3天,每部收費1.5元,超過3天的部分每天收費1.5元

積分計算規則:

       每借一步電影積分加1,新片每部加2

statement()函數中所做的事情就是根據上面的計算規則,根據使用者所租賃的電影的不同來進行金額的計算和積分的計算的。

代碼重構(六):代碼重構完整案例
如果你看代碼不太直覺的話,下面我使用了startUML簡單的畫了一個UML的類圖來說明上述三個類中的依賴關系。具體如下所示:
代碼重構(六):代碼重構完整案例
在對上面代碼重構之前呢,我們還必須有上述代碼的測試用例。因為在每次重構之前,我們修改的是代碼的内部結構,而代碼子產品對外的調用方式不會變的。是以我們所建立的測試用例可以幫助驗證我們重構後的程式是否可以正常的工作,是否重構後還符合我們的需求。下方就是我們建立的測試用例(當然,在iOS開發中你可以使用其他的測試架構來進行單元測試,重構時,單元測試是少不了的)。在本篇部落格中重構後的代碼仍然使用下方的測試用例。

1 //測試用例--------------------------------------------------------------------
 2 //建立使用者
 3 let customer = Customer(name: "ZeluLi")
 4 
 5 //建立電影
 6 let regularMovie:Movie = Movie(title: "《老炮兒》", priceCode: Movie.REGULAR)
 7 let newMovie:Movie = Movie(title: "《福爾摩斯》", priceCode: Movie.NEW_RELEASE)
 8 let childrenMovie:Movie = Movie(title: "《葫蘆娃》", priceCode: Movie.CHILDRENS)
 9 
10 //建立租賃資料
11 let rental1:Rental = Rental(movie: regularMovie, daysRented: 5)
12 let rental2:Rental = Rental(movie: newMovie, daysRented: 8)
13 let rental3:Rental = Rental(movie: childrenMovie, daysRented: 2)
14 
15 customer.rentals.append(rental1)
16 customer.rentals.append(rental2)
17 customer.rentals.append(rental3)
18 
19 let result = customer.statement()
20 print(result)      

針對上述案例,上面測試用例的輸出結果如下。在每次重構後,我們都會執行上述測試代碼,然後觀察結果是否與之前的相同。當然如果你的是單元測試的話,完全可以把對結果檢查的工作交給單元測試中的斷言來做。

代碼重構(六):代碼重構完整案例

二、重構1:對較statement函數進行拆分

1.對statement()函數使用“Extract Method”原則

在上面的案例中,最不能容忍的,也就是最需要重構的首先就是Customer中的statement()函數。statement()函數最大缺點就是函數裡邊做的東西太多,我們第一步需要做的就是對其進行拆分。也就是使用我們之前提到過的“Extract Method”(提煉函數)原則對該函數進行簡化和拆分。将statement()中可以獨立出來的子產品進行提取。經過分析後的,我們不難發現下方紅框當中的代碼是一個完整的子產品,一個是進行單價計算的,一個是進行積分計算的,我們可以将這兩塊代碼進行提取并封裝成一個新的方法。在封裝新方法時,要給這個新的方法名一個恰當的函數名,見名知意。

代碼重構(六):代碼重構完整案例

下方這塊代碼就是我們對上面這兩個紅框中的代碼的提取。在提取時,将依賴于statement()函數中的資料作為新函數的參數即可。封裝後的方法如下,在statement函數中相應的地方調用下方的方法即可。下方就是我們封裝的計算目前電影金額和計算積分的函數。這兩個函數都需要傳入一個Rental的對象。

//根據租賃訂單,計算目前電影的金額
    func amountFor(aRental: Rental) -> Double {
        
        var result:Double = 0       //單價變量
        
        switch aRental.movie.priceCode {
            case Movie.REGULAR:
                result += 2
                if aRental.daysRented > 2 {
                    result += Double(aRental.daysRented - 2) * 1.5
                }
            case Movie.NEW_RELEASE:
                result += Double(aRental.daysRented * 3)
            case Movie.CHILDRENS:
                result += 1.5
                if aRental.daysRented > 3 {
                    result += Double(aRental.daysRented - 3) * 1.5
                }
            default:
                break
        }
        return result
    }
    

     //計算目前電影的積分
    func getFrequentRenterPoints(rental: Rental) -> Int {
        var frequentRenterPoints: Int = 0               //使用者積分
        frequentRenterPoints++
        if rental.movie.priceCode == Movie.NEW_RELEASE &&
            rental.daysRented > 1{
                frequentRenterPoints++
        }
        return frequentRenterPoints
    }      

經過上面的重構步驟,我們會運作一下測試用例或者執行一下單元測試,看是否我們的重構過程引起了新的bug。

三、重構2:将相應的方法移到相應的類中

經過上面的重構,我們從statement()函數中提取了兩個方法。觀察這兩個重構後的方法我們不難看出,這兩個封裝出來的新的方法都需要一個參數,這個參數就是Rental類的對象。也就是這兩個方法都依賴于Rental類,而對該函數所在的目前類不太感冒。出現這種情況的原因就是這兩個函數放錯了地方,因為這兩個函數放在Customer類中不依賴與Customer類而依賴于Rental類,那就足以說明這兩個方法應該放在Rental類中。

經過我們簡單的分析後,我們就可以決定要将我們新提取的方法放到Rental類中,并且函數的參數去掉。因為函數在Rental類中,是以在函數中直接使用self即可。将計算金額的方法和計算積分的方法移到Rental類中後,我們的Rental類如下所示。在我們的Customer中的statement()方法中在計算金額和計算積分時,直接調用Rental中的方法即可。經過這一步重構後,不要忘記執行一下你的測試用例,監測一下重構的結果是否正确。

代碼重構(六):代碼重構完整案例

四、使用“以查詢取代臨時變量”再次對statement()函數進行重構

經過第二步和第三步的重構後,Customer中的statement()函數如下所示。在計算每部電影的金額和積分時,我們調用的是Rental類的對象的相應的方法。下方的方法與我們第一部分的方法相比可謂是簡潔了許多,而且易于了解與維護。

代碼重構(六):代碼重構完整案例

不過上面的代碼仍然有重構的空間,舉個例子,如果我們要将結果以HTML的形式進行組織的話,我們需要将上面的代碼進行複制,然後修改result變量的文本組織方式即可。但是這樣的話,其中的好多臨時變量也需要被複制一份,這是完全相同的,這樣就容易産生重複的代碼。在這種情況下,我們需要使用“Replace Temp with Query”(已查詢取代臨時變量)的重構手法來取出上面紅框中的臨時變量。

上面紅框中的每個臨時變量我們都會提取出一個查詢方法,下方是使用“Replace Temp with Query”(已查詢取代臨時變量)規則重構後的statement()函數,以及提取的兩個查詢函數。

代碼重構(六):代碼重構完整案例

經過上面這些步驟的重構,我們的測試用例依然不變。在每次重構後我們都需要調用上述的測試用例來檢查重構是否産生了副作用。現在我們的類間的依賴關系沒怎麼發生變化,隻是相應類中的方法有些變化。下方是現在代碼所對應的類圖,因為在上述重構的過程中我們主要做的是對函數的重構,也就是對函數進行提取,然後将提取的函數放到相應的類中,從下方的簡化的類圖中就可以看出來了。

代碼重構(六):代碼重構完整案例

五. 繼續将相應的函數進行移動(Move Method)

對重構後的代碼進行觀察與分析,我們任然發現在Rental類中的getCharge()函數中的内容與getFrequentRenterPoints()函數中的内容對Movie類的依賴度更大。因為這兩個函數都隻用到了Rental類中的daysRented屬性,而多次用到了Movie中的内容。是以我們需要将這兩個函數中的内容移到Movie類中更為合适。是以我繼續講該部分内容進行移動。

移動的方法是保留Rental中這兩個函數的聲明,在Movie中建立相應的函數,将函數的内容移到Movie中後,再Rental中調用Movie中的方法。下方是我們經過這次重構後我們Movie類中的内容。其中紅框中的内容是我們移過來的内容,而綠框中的參數需要從外界傳入。

代碼重構(六):代碼重構完整案例

将相應的方法體移動Movie類中後,在Rental中我們需要對其進行調用。在調用相應的方法時傳入相應的參數即可。下方就是經過這次中國Rental類的代碼,綠框中的代碼就是對Movie中新添加的方法的調用。

代碼重構(六):代碼重構完整案例

經過上面的重構,我們的方法似乎是找到了歸宿了。重構就是這樣,一步步來,不要着急,沒動一步總是要向着好的方向發展。如果你從第一部分中的代碼重構到第五部分,似乎有些困難。經過上面這些間接的過程,感覺也是挺愉快的蠻。下方是經過我們這次重構的類圖。

代碼重構(六):代碼重構完整案例

六、使用“多态”取代條件表達式

在我們之前的部落格中對條件表達式進行重構時,提到了使用類的多态對條件表達式進行重構。接下來我們就要使用該規則對Movie類中的getCharge()與getFrequentRenterPoints()函數進行重構。也就是使用我們設計模式中經常使用的“狀态模式”。在該部分我們不需要對Rental類和Customer類進行修改,隻對Movie類修改,并且引入相應的接口和繼承關系。

我們對Movie類中的getCharge()方法中的Switch-Case結構觀察時,我們很容易發現,此處完全可以使用類的多态來替代(具體請參見《代碼重構(四):條件表達式重構規則(Swift版)》)。具體實作方式是将不通的價格計算方式提取到我們新建立的價格類中,每種電影都有自己價格類,而這些價格類都實作同一個接口,這樣一來在Movie中就可以使用多态來擷取價格了。積分的計算也是一樣的。下方是我們要實作結構的類圖。下方紅框中是在原來基礎上添加的新的接口和類,将條件表達式所處理的業務邏輯放在了我們新添加的類中。這樣我們就可以使用類的多态了,而且遵循了“單一職責”。

代碼重構(六):代碼重構完整案例

下方代碼就是上面大的紅框中所對應的代碼實作。Price是我們定義好的協定,在協定中規定了遵循該協定的類要實作的方法。而在每個具體實作類中實作了相同的接口,但是不同的類中相同的方法做的事情不同。在不同的類中的getCharge()中要做的事情就是Switch-Case語句中所處理的資料。

代碼重構(六):代碼重構完整案例

添加上上面的結構以後,在麼我們的Movie中就可以使用多态了,在Movie中添加了一個Price聲明的對象,我們會根據不同的priceCode來給price變量配置設定不同的對象。而在getCharge()中隻管調用price的getCharge()函數即可,具體做法如下。

代碼重構(六):代碼重構完整案例

今天的部落格到這兒也就差不多了,其實上面的代碼仍然有重構的空間,如果我們想把Switch-Case這個結構去掉的話,我們可以在上面代碼的基礎上建立多個工廠方法即可。在此就不過贅述了。

如果看完今天的部落格的内容不夠直覺的話,那麼請放心。本篇部落格中每次重構過程的完整執行個體會在github上進行分享。對每次重構的代碼都進行了系統的整理。今天部落格中的代碼整理的結果如下。

代碼重構(六):代碼重構完整案例

github分享位址為:https://github.com/lizelu/CodeRefactoring-Swift

作者:青玉伏案

出處:http://www.cnblogs.com/ludashi/

本文版權歸作者和共部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利。

如果文中有什麼錯誤,歡迎指出。以免更多的人被誤導。

收履歷:某網際網路公司,招聘iOS/Android靠譜工程師,入職後,可内部聯系樓主,有小禮品贈送,有意者可郵箱投遞履歷:[email protected]