無論做什麼事情呢,都要善始善終呢。前邊連續發表了5篇關於重構的博客,其中分門別類的介紹了一些重構手法。今天的這篇博客就使用一個完整的示例來總結一下之前的重構規則,也算給之前的關於重構的博客畫一個句號。今天的示例借鑒於《重構,改善既有代碼的設計》這本書中的第一章的示例,在其基礎上做了一些修改。今天博客 ...
無論做什麼事情呢,都要善始善終呢。前邊連續發表了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