重構是項目作到必定程度後必然要作的事情。代碼重構,能夠改善既有的代碼設計,加強既有工程的可擴充、可維護性。隨着項目需求的不斷迭代,需求的不斷更新,咱們在項目中所寫的代碼也在時時刻刻的在變化之中。在一次新的需求中,你添加了某些功能模塊,但這些功能模塊有可能在下一次需求中不在適用。或者你由於需求迭代與變動,使你原有的方法或者類變得臃腫,以及各個模塊或者層次之間耦合度增長。此時,你要考慮重構了。html
重構,在《重構,改善既有代碼的設計》這本經典的書中給出了定義,大概就是:在不改變代碼對外的表現的狀況下,修改代碼的內部特徵。說白了,就是咱們的測試用例不變,而後咱們對既有的代碼的結構進行修改。重構在軟件開發中是常常遇到的,也是很是重要的。在需求迭代,Debug,Code Review時,你均可以對你既有的代碼進行重構。git
在接下來的幾篇博文中,我想與你們一塊去窺探一下代碼重構的美麗,學習一下代碼重構的一些規則。固然在每一個規則中都有小的Demo, 在本篇博客以及相關內容的博客是使用Swift語言實現的。固然,和設計模式相同,重構重要的是手法和思想,和使用什麼樣的語言關係不大。經典的重構書籍中是使用Java語言來實現的,若是你對PHP, Python等其餘語言比較熟悉,徹底能夠使用這些語言來測試一些重構手法。github
本篇博客的主題就是經過一些列的重構手法,對既有的須要重構的函數或者方法進行重構。而且會將每一個示例在GitHub上進行分享,感興趣的小夥伴能夠對其進行下載。有的小夥伴說了,我沒有Mac,怎麼對你寫的Swift代碼進行編譯呢?這個問題好解決,你能夠看我以前發表的這篇博客《窺探Swift之使用Web瀏覽器編譯Swift代碼以及Swift中的泛型》。你能夠將相關代碼進行拷貝,在瀏覽器中觀察結果。由於在線編譯的網站是國外的,訪問起來也許會有一些卡頓,不過是能夠用的。好前面扯了這麼多了,進入今天的主題。編程
1、Extract Method(提取函數)-------將大函數按模塊拆分紅幾個小的函數設計模式
Extract Method被翻譯成中文就是提取函數的意思,這一點在代碼重構中用的很是很是的多。在重構時提倡將代碼模塊進行細分,由於模塊越小,可重用度就越大。不要寫大函數,若是你的函數過大,那麼這意味着你的函數須要重構了。由於函數過大,可維護性,可理解性就會變差。而且當你實現相似功能的時候就容易產生重複代碼。寫代碼時,最忌諱的就是代碼重複。這也就是常常所說的DRY(Don`t Repeat Yourself)原則。因此當函數過長時,你須要將其細分,將原函數拆分紅幾個函數。數組
下方將會經過一個示例來直觀的感覺一下Extract Method,固然這些示例不是我原創的,是《重構:改善既有代碼的設計》中Java示例演變的Swift版,在寫Swift代碼時,對原有的示例進行了一些修改,算是僞原創吧。不過目的只有一個:但願與你們交流分享。實在是沒有必要再找其餘的例子說明這些重構規則,由於《重構:改善既有的代碼的設計》這本書真的是太經典了。
瀏覽器
1.須要重構的代碼以下所示。下方代碼中的MyCustomer類中有兩個常量屬性,而且該類提供了一個構造器。該類還提供了一個輸出方法,就是第一該類中的屬性進行打印說明,其實該類中沒有什麼功能。安全
在寫好須要重構的類後,咱們要爲該類寫一個測試用例。這便於在咱們重構時對重構的正確性進行驗證,由於每次重構後都要去執行該測試用例,以保證咱們重構是正確的。下方截圖就是爲上方示例寫的測試用例以及該測試用例的打印結果。固然重構後咱們也須要調用該測試用例,並觀察打印結果是否與以前的一致。固然若是你不想本身觀察,你能夠爲上面的類添加相應的單元測試,這也是在常規項目中常用的。至於若是添加測試用例,咱們會在後面的博客中進行詳細介紹。下方就是上述類的測試用例和輸出結果:數據結構
2.接下來咱們對上面類中的printOwning函數進行分析。上述類能夠正常工做,這是確定的。可是printOwning()函數寫的不夠好。由於它幹了太多的事情,也就是說該函數包括了其餘子模塊,須要對其進行拆分。由上面截圖中的紅框能夠看出,每一個紅框就表明着一個獨立的功能模塊,就說明這一塊代碼能夠被拆分紅獨立的函數。在拆分子函數時,咱們要爲該函數起一個與改代碼塊功能相符合的名字。也就是說當你看到該函數名字時,你就應該知道該函數是幹嗎的。架構
下方代碼段就是咱們重構後的類。說白的,就是對函數中能夠進行獨立的模塊進行提取,併爲提取的新的函數命一個合適的名稱。通過重構後printOwing()函數就只有兩行代碼,這樣看其中調用的函數名也是很容易理解其做用的。下方拆分出來的三個函數也是一個獨立的模塊,由於函數短小,因此易於理解,同時也易於重用。通過Extract Method,固然好處是多多的。通過重構後的代碼,我在調用上述的測試用例,輸出結果和原代碼是一直的,若是不一致的話,那麼說明你的重構有問題呢,須要你進行Debug。
二. Inline Method ---- 內聯函數:將微不足道的小函數進行整合
看過《周易》的小夥伴應該都知道,《周易》所表達的思想有一點就是「物極必反」。《周易》中的六十四卦中的每一卦的「上九」(第六位的陽爻)或者「上六」(第六位的陰爻)都是物極必反的表現。其實《周易》其實就是計算機科學中二進制的表象,由於太極生兩儀(2進制中的2),兩儀生四象(2的平方爲4),四象生八卦(4 x 2 = 8),八卦有演變出六十四卦。六十四卦的就是2進制中的0-1排列。九五至尊,九六就物極必反了。wo kao, 扯遠了,言歸正傳,固然這提到《周易》不是說利用周易如何去算卦,如何去預測,本寶寶不信這東西。不過《周易》中的哲學仍是頗有必要學習一下的。有所取,有所不取。
回到本博客的主題,Inline Method實際上是和Extract Method相對的。當你在重構或者平時編程時,對模塊進行了過分的封裝,也就是使用Extract Method有點過頭了,把過於簡單的東西進行了封裝,好比一個簡單的布爾表達式,並且該表達式只被用過一次。此時就是過分的使用Extract Method的表現了。物極必反,因此咱們須要使用Inline Method進行中和,將過分封裝的函數在放回去,或者將那些沒有必要封裝的函數放回去。也就是Extract Method相反的作法。
至於Inline Method規則的示例呢,在此就不作過多的贅述了,由於只須要你將Extract Method的示例進行翻轉便可。
三.Replace Temp with Query----以查詢取代臨時變量: 將一些臨時變量使用函數替代
1.Replace Temp with Query說白了就是將那些有着複雜表達式賦值而且屢次使用的臨時變量使用查詢函數取代,也就是說該臨時變量的值是經過函數的返回值來獲取的。這樣一來在實現相似功能的函數時,這些複雜的臨時變量就能夠進行復用,從而減小代碼的重複率。下方就是Replace Temp with Query規則的一個特定Demo,接下來咱們要對getPrice()函數使用Replace Temp with Query規則進行重構。
對上面的小的demo建立對應的測試用例是少不了的,由於咱們要根據測試用例還測試我重構後的代碼是否一致,下方截圖就是該代碼的測試用例以及輸出結果,具體以下所示。
2.接下來就是對Procut類中的getPrice()函數進行分析並重構了。在getPrice()函數中的第一個紅框中有一個basePrice臨時常量,該常量有一個較爲複雜的賦值表達式,咱們能夠對其使用Replace Temp with Query進行重構,可就是建立一個函數來返回該表達式的值。第二個紅框中的discountFactor臨時變量被屢次使用,咱們能夠對其經過Replace Temp with Query規則進行重構,具體重構後的代碼以下所示。
由重構後的代碼容易看出,上面咱們提到的臨時常量或者變量都不存在了,取而代之的是兩個查詢方法,對應的查詢方法返回的就是以前消除的臨時變量或常量的值。
4、Inline Temp ---內聯臨時變量:與上面的Replace Temp with Query相反
當臨時變量只被一個簡單的表達式賦值,而該臨時變量妨礙了其餘重構手法。此時咱們就不該該使用Replace Temp with Query。之因此有時咱們會使用到Inline Temp規則,是由於Replace Temp with Query規則使用過分形成的狀況,仍是物極必反,使用Replace Temp with Query過分時,就須要使用Inline Temp進行修正,固然Inline Temp的示例與Replace Temp with Query正好相反,在此就不作過多的贅述了。
5、Introduce Explaining Variable---引入解釋性變量:將複雜的表達式拆分紅多個變量
當一個函數中有一個比較複雜的表達式時,咱們能夠將表達式根據功能拆分紅不一樣的變量。拆分後的表達式要比以前未拆分的表達式的可讀性更高。將表達式拆分紅相應的臨時變量,也就是Introduce Explaining Variable,若是臨時變量被屢次使用的話,咱們還能夠嘗試着使用Replace Temp with Query規則去除臨時變量,也就是將臨時變量換成查詢函數。
1.在下方Product類中的getPrice()方法中返回了一個比較長的表達式,第一眼看這個函數感受會很是的不舒服。由於它返回的表達式太長了,並且可讀性不太好。在這種狀況下就頗有必要將該表達式進行拆分。
2.接下來就能夠使用Introduce Explaining Variable規則,引入解釋性變量。顧名思義,咱們引入的變量是爲了解釋該表達式中的一部分的功能的,目的在於讓該表達式具備更好的可讀性。使用Introduce Explaining Variable規則,就至關於爲該表達式添加上相應的註釋。下方截圖就是使用 Introduce Explaining Variable規則進行重構後的結果。
3.引入臨時變量是爲了更好的可讀性,若是臨時變量所表明的表達式屢次使用,咱們就能夠對上述函數在此使用Replace Temp with Query規則進行重構。也就是去除常用並且表達式比較複雜的臨時變量,下方代碼段是對上述函數進行Replace Temp with Query重構,去掉臨時變量,再次重構後的結果以下所示。
6、Split Temporary Variable-----分解臨時變量:一心不可二用
什麼叫分解臨時變量的,具體說來就是在一個函數中一個臨時變量不能作兩種事情,也就是一個臨時變量不能賦上不一樣意義的值。若是你這麼作了,那麼對不起,請對該重複使用的臨時變量進行分解,也就是說你須要建立一個新的臨時變量來接收第二次分配給第一個臨時變量的值,併爲第二個臨時變量命一個確切的名字。
下方第一個函數是重構前的,能夠看出temp被重複的賦值了兩次的值,若是這兩個值關係不大,並且temp不足以對兩個值的意思進行說明。那麼就說明該段代碼就應該被重構了。固然,重構的作法也是很是簡單的,只須要術業有專攻便可,各司其職,而且爲每一個臨時變量命一個合適的名字便可。具體作法以下所示。
7、Remove Assignments to Parameters----移除對參數的賦值
「移除對參數的賦值」是什麼意思呢?顧名思義,就是在函數中不要對函數參數進行賦值。也就是說你在函數的做用域中不要對函數的參數進行賦值(固然,輸入輸出參數除外),當直接對函數的參數進行修改時,對不起,此時你應該對此重構。由於這樣會是參數的原始值丟失,咱們須要引入臨時變量,而後對這個臨時變量進行操做。
1.下方這個discount()函數就作的很差,由於在discount()函數中直接對非inout參數inputVal進行了修改而且返回了,咱們不建議這樣作。遇到這種狀況,咱們須要使用Remove Assignments to Parameters規則對下方的函數進行重構。
2.固然重構的手法也特別簡單,就是須要將上面的inputVal使用函數的臨時變量進行替代便可,下方就是重構後的函數。
八.Replace Method with Method Object----以函數對象取代函數
當一個特別長的函數,並且函數中含有比較複雜的臨時變量,使用上述那些方法很差進行重構時,咱們就要考慮將該函數封裝成一個類了。這個對應的類的對象就是函數對象。咱們能夠將該場函數中的參數以及臨時變量轉變成類的屬性,函數要作的事情做爲類的方法。將函數轉變成函數類後,咱們就能夠使用上述的某些方法對新的類中的函數進行重構了。具體作法請看下方示例。
1.下方示例中的discount函數有過多的參數(固然,現實項目工程中參數比這個還要多),並函數中含有多個臨時變量,假設函數功能比較複雜,並且比較長。下方示例對該函數使用上述那些規則進行重構會比較複雜,此時咱們就能夠將該函數抽象成一個類。
2.重構的第一步就是將上述discount()函數抽象成Discount類。在Discount類中有六個屬性,這六個屬性分別對應着discount()函數的不一樣參數。除了添加參數屬性外,咱們在函數類提取時還添加了一個Account的委託代理對象。該委託代理對象是爲了在Discount類中訪問Account類中依賴的數據,下方是第一次重構後的代碼。
3.接着,咱們就能夠在新的Discount類中的compute()方法中使用咱們上述介紹的規則進行重構。對compute()方法進行分析,咱們發現importandValue等屬性是能夠經過Replace Temp with Qurey 規則進行消除的。所爲咱們能夠再次對上述方法進行重構,重構後的具體代碼以下:
在上篇博客《代碼重構(一):函數重構規則(Swift版)》中,詳細的介紹了函數的重構規則,其中主要包括:Extract Method, Inline Method, Inline Temp, Replace Temp with Query, Introduce Explaining Variable, Split Temporary Variable, Remove Assignments to Parameters, Replace Method with Method Object等。關於上述這些函數重構的規則更爲詳細的信息請參考上一篇博客,在此就不作過多的贅述了。
今天這篇博客主要介紹一下類的重構。在咱們寫代碼時,有些類是不規範的,須要重構。在對類進行重構時,也是有一些章法可尋的,本篇博客就結合着相關示例,對類的重構進行相關的介紹。固然在本篇博客中使用的實例,仍是延續上一篇文章的風格,仍然採用Swift語言進行編寫。固然,仍是那句話,重構的思想和手法與設計模式相似,都與具體語言實現無關。舉一反三,關鍵仍是思想和手法。爲了精簡博文的篇幅,相關的測試用例就不往上粘貼了。固然,在你實現時,測試用例是必不可少的,由於測試用例能夠在你重構時及時發現由於重構而產生的錯誤。言歸正傳,進入今天博客的主題。
1、Move Method----方法遷移
關於Move Method,首先談論一下爲何要進行方法的遷移。緣由很簡單,就是當類中的方法不適合放在當前類中時,就應該爲該方法尋找合適下家。那麼怎樣才能夠稱做是當前方法不適合在當前類中呢?一個類中的函數與另外一個類有不少的交互,函數很是依賴於某個類。若是一個類有太多行爲,或者與另外一個類有太多合做而造成高度耦合。此時就應該將該方法搬移到其高度依賴的類中。
在給方法搬家時須要作的就是在方法的新家中建立一個方法,實現要搬移的功能,若是新建立的函數須要舊類中的數據,那麼就建立一個委託對象來解決這個問題。說白了就是在另外一個類中建立一個相同的功能的新函數,將舊函數變成一個單純的委託函數,或者將舊函數徹底移除。搬移後,咱們能夠再使用函數的重構規則對新組的函數進行重構。下方就經過一個實例來直觀的感覺一下Move Method。
1.代碼實例
在下方截圖中有兩個類,一個Book類,另外一個是BookCustomer類。在Book類中有兩個屬性,一個是bookCode:表示書的種類(NEW_BOOK,OLD_BOOK, CHIDREN_BOOK), 另外一個屬性就是書名bookName。在BookCustomer中有3個字段,name表示用戶的名稱,isVip表示用戶是不是會員,books表示該用戶所購買的書的集合。BookCustomer類中的charge()方法用來根據books數組來計算圖書的總價格,並返回總價格。若是是VIP, 就在總價格的基礎上打7折,普通用戶打8折。下方截圖就是其具體實現。
2.使用Move Method進行重構
首先咱們對上述兩個類進行分析,觀察須要重構的地方。首先第一眼看代碼時,較長的charge()函數會讓咱們看起來些微的不舒服,由於它太長了。再仔細分析,其中的Switch語句中的業務邏輯用的全是Book類的東西,和當前BookCustomer類沒用什麼關聯。可是這個Switch語句是當前charge()函數的核心,也就是BookCustomer嚴重依賴Book類的地方。以此分析下去,咱們就清楚的指定,該Switch語句塊放錯了地方,它應該放在Book類中。因此咱們應該將這塊代碼進行搬移。
重構方法就是在Book類中建立一個charge()函數,將Switch語句塊放入新的charge()函數中。而後在原來的charge()函數使用Switch語句時調用新的charge()方法。下方代碼段是使用Move Method重構後的結果。
3.使用函數重構
在使用Move Method重構後,咱們看出在BookCustomer類中的charge()函數是能夠使用Extract Method和Replace Temp With Qurey進行重構的。關於這兩個函數重構的規則的具體細節請參見《代碼重構(一):函數重構規則(Swift版)》中的介紹。下方截圖是對BookCustomer類中的charge()函數進行重構後的結果,以下所示:
2、Move Field----搬移字段
上一部分是搬移方法,Move Field(搬移字段)與Move Method適用場景相似。當在一個類中的某一個字段,被另外一個類的對象頻繁使用時,咱們就應該考慮將這個字段的位置進行更改了。Move Field與Move Method的思想和作法差很少,再次對其的示例就省略了。觸類旁通,你能夠類比着Move Method來使用Move Field規則。具體實現方式在此就不作過多的贅述了。
3、Extract Class----提煉類
Extract Class和Extract Method相似,Extract Method提取的是方法,而Extract Class提取的是類。一個類若是過於複雜,作了好多的事情,違背了「單一職責」的原則,因此須要將其能夠獨立的模塊進行拆分,固然有可能由一個類拆分出多個類。固然,對類的細化也是爲了減小代碼的重複性,以及提升代碼的複用性,便於代碼的維護。下方將會經過一個實例,對類進行提煉。
1.重構前的代碼
下方是咱們將要進行重構的代碼段。在Person類中有三個字段,常量name表示該Employee的名字,officeAreaCode表示Employee所在辦公部門的區域代碼。而後就是Employee類的構造函數了。Employee類比較簡單。
2.使用Extract Class對Employee重構
接下來要作的就是使用Extract Class對Employee進行重構。由於上述Employee類設計的很差,由於Employee類能夠再分。顯然能夠將區域號和電話號提取成一個TelePhoneNubmer類,在Employee中調用TelePhoneNubmer類。這樣一來TelePhoneNubmer類就能夠重複利用了,並且層次結構更爲清晰。下方代碼段就是對上述代碼進行重構後的結果。具體以下所示:
4、Inline Class----類的內聯化
又到了「物極必反」的時候了。Extract Method與Inline Method職責相反,Extract Class固然也就任責相反的原則。那就是接下來要介紹的類的內聯化:Inline Class。若是過分使用Extract Class原則的話,會使得某些類過於簡單而且調用該簡單的類的地方極少。也就是說一個類根本不能稱爲一個類,因此咱們能夠經過Inline Class將過分抽象出來的類放到其餘類中。
關於Inline Class的示例在此就不作過多的贅述了,由於與Extract Class原則相反,將第三部分中的示例倒着過一遍即爲類的內聯化的工做方式。
5、Hide Delegate----隱藏「委託關係」
隱藏類之間的「委託關係」這一原則用起來是很是不錯的,它能夠簡化類調用委託者的方式。簡單的說就是講委託調用的鏈,封裝成相應的方法,使其隱藏掉具體的調用細節,從而簡化了調用方式。下方會根據具體事例和測試用例來介紹一下Hide Delegate。
1.重構前的案例
在下方代碼片斷中有兩個類,這兩個類互爲依賴關係。Department中有People,該People對應的就是經理人。還有一個字段就是chargeCode,對應的是部門代碼。而People類中有name--名字字段,department--所屬部門字段。在People對象中能夠委託department對象來獲取經理的名字。
獲取People對象所在部門經理的名字的測試用例以下所示。在下方測試用例中建立了一個經理和一個員工,併爲員工和經理綁定關係。zeluLi.department.manager.name就是委託department對象來調用經理的名字,這樣調用未免太長,因此有必要使用Hide Delegate原則對其進行優化。
2.使用Hide Delegate進行重構
使用Hide Delegate進行重構的方式是比較簡單的,就是在People中封裝一個方法,在方法中返回經理的對象便可,這樣就隱藏掉了委託關係。具體實現方式以下截圖所示:
添加上上面的函數後的調用方式以下:
Remove Middle Man(移除中間人)原則與Hide Delegate相反,就是沒有必要將委託人進行隱藏,因此就使用Remove Middle Man原則將上面咱們封裝的獲取委託人的方法進行移除。關於Remove Middle Man的範例就不作過多的贅述了。
6、Introduce Foreign Method----引入外加函數
這一點在開發中用的仍是比較多的,有時候你在不想或者不能修改原類的狀況下想爲該類添加新的方法。在這種狀況下就會使用到Introduce Foreign Method。在Swift語言中,使用Introduce Foreign Method原則特別簡單,也就是在不改變類的狀況下對類進行擴展也是特別簡單的。由於Swift語言以及OC中有延展的功能,因此很是對此很是好實現的。下方的代碼段就是對MyTest類使用extension爲其擴展一個method2方法,具體以下所示。
在《代碼重構(一):函數重構規則(Swift版)》和《代碼重構(二):類重構規則(Swift版)》中詳細的介紹了函數與類的重構規則。本篇博客延續以前博客的風格,分享一下在Swift語言中是如何對數據進行重構的。對數據重構是頗有必要的,由於咱們的程序主要是對數據進行處理。若是你的業務邏輯很是複雜,那麼對數據進行合理的處理是頗有必要的。對數據的組織形式以及操做進行重構,提升了代碼的可維護性以及可擴展性。
與函數重構與類重構相似,對數據結構的重構也是有必定的規則的。經過這些規則能夠使你更好的組織數據,讓你的應用程序更爲健壯。在本篇博客中將會結合着Swift代碼實現的小實例來分析一下數據重構的規則,並討論一下什麼時候使用那些重構規則進行數據重構。仍是那句話「物極必反」呢,若是不恰當的使用重構規則,或者過分的使用重構規則不但起不到重構的做用,有時還會起到副作用。廢話少說,進入今天數據重構的主題。
一. Self Encapsulate Field (自封裝字段)
"自封裝字段"理解起來比較簡單,一句話歸納:雖然字段對外是也隱藏的,可是仍是有必要爲其添加getter方法,在類的內部使用getter方法來代替self.field,該方式稱爲自封裝字段,本身封裝的字段,本身使用。固然該重構規則不是必須執行的,由於若是你直接使用self來訪問類的屬性若是不妨礙你作擴展或者維護,那麼也是能夠的,畢竟直接訪問變量更爲容易閱讀。各有各的好處,間接訪問字段的好處是使你的程序更爲模塊化,能夠更爲靈活的管理數據。好比在獲取值時,由於後期需求的變化,該獲取的字段須要作一些計算,那麼間接訪問字段的方式就很容易解決這個問題,而直接訪問字段的方式就不是很好解決了。因此間接一下仍是好處多多的,不過直接訪問不影響你的應用程序的話,也是無傷大雅的。
下方會經過一個實例來看一下間接訪問字段的好處。下方的IntRange類中的字段就沒有提供間接訪問的方法,在代碼中經過直接訪問的形式來使用的字段。這種作法對當前的程序影響不大,可是若是提出需求了。要在high賦值後,在IntRange對類進行一個較爲複雜的修改。那麼對於下方代碼而言,有兩種解決方案,就是在構函數中進行修改,在一個就是在使用self.high的地方進行修正,固然這兩種方法都不理想。最理性的方案是在相應字段的getter方法修改。
下方截圖就是爲InRange類中相應的字段自封裝了getter和setter方法,並在使用self.字段的地方使用該自封裝的方法代替(構造函數中對字段的初始化除外,由於設置方法通常在對象建立完畢之後在調用,因此不能在建立對象時調用,固然Swift語言也不容許你在構造函數函數中調用設置方法)。下方紅框中的是咱們添加的自封裝方法,綠框中是對自封裝方法的使用,白框中是須要注意的一點,構造函數中不能使用該設置函數。
固然,只添加上上述自封裝字段後,優勢不明顯。固然子類CappedRange繼承了IntRange函數後,這種優勢就被顯示了出來。在子類中CappedRange的high須要與新添加的字段cap進行比較,取較大的值做爲區間的上限。在這種狀況下自封裝字段的優勢就被凸顯了出來。在子類中只須要對getHigh()函數進行從新,在重寫的方法中進行相應的計算便可。由於當在子類中調用inclued()方法時,在include()方法中調用的是子類的getHigh()方法。具體請看下方子類截圖:
二. Replace data Value with Object(以對象取代數據值)
「以對象取代數據值」說白了就是咱們常說的實體類,也就是Model類。Model的職責就將一些相關聯的數據組織在一塊兒來表示一個實體。Model類比較簡單,通常只用於數據的存儲,其中有一些相關聯的字段,併爲這些相關聯的字段添加getter/和setter方法。下方是一個Person的數據模型,咱們命名爲PersonModel,其中有三個表示Person屬性的字段name、birthday、sender。而後提供了一個構造器以及各個屬性對應的getter和setter方法。具體請看下方代碼所示:
3、Change Value to Reference (將值對象改變成引用對象)
在介紹「將值對象改變成引用對象」以前,咱們先去了解一下值對象和引用對象的區別。先說一下值對象,好比兩個相等的數值,存入了兩個值對象中,這兩個值對象在內存中分別佔有兩塊不一樣的區域,因此改變其中一個值不會引發另外一個值得變化。而引用對象正好相反,一個內存區域被多個引用指針所引用,這些引用指針即爲引用對象,由於多個指針指向同一塊內存地址,因此不管你改變哪個指針中的值,其餘引用對象的值也會跟着變化。
基於值對象和引用對象的特色,咱們有時候根據程序的上下文和需求須要將一些值類型改變成引用類型。由於有時候須要一些類的一些對象在應用程序中惟一。這和單例模式又有一些區別,單例就是一個類只能生成一個對象,而「將值對象改變成引用對象」面臨的就是類能夠建立多個對象,可是這多個對象在程序中是惟一的,而且在某一個引用點修改對象中的屬性時,其餘引用點的對象值也會隨之改變。下方就經過一個訂單和用戶的關係來觀察一下這個規則。
1. 值引用的實例
(1) 首先咱們須要建立一個消費者也就是Customer類。Customer類比較簡單,其實就是一個數據實體類。其中有name和idCard屬性並對應着getter/setter方法,具體代碼以下所示:
(2)、緊接着咱們須要建立一個訂單類,在訂單建立時咱們須要爲該訂單關聯一個Customer(固然這爲了簡化實例,咱們省略了Order中的其餘字段)。該Order類的代碼也是比較簡單的在此就不作過的的贅述了。不過有一點須要注意的是爲了測試,咱們將customer設計成值類型,也就是每一個Order中的customer都會佔用不一樣的內存空間,這也就是值類型的特色之一。
(3).建立完Order與Customer類後,緊接着咱們要建立測試用例了。並經過測試用例來發現問題,並在重構時對該問題進行解決。在測試用例中咱們建立了三個訂單,爲每一個訂單關聯一個Customer。從測試用例中能夠看出,關聯的消費者數據爲同一我的,可是這一我的在內存中佔用了不一樣的存儲空間,若是一個訂單中的用戶信息進行了更改,那麼其餘訂單中的用戶信息是不會更新的。若是建立完用戶後,信息不可更改,雖然浪費點存儲空間,可是使用值類型是沒用問題的。一旦某個訂單修改了用戶名稱,那麼就會出現數據不一樣步的問題。
2.將Order中Customer改成引用類型(從新設計Order類)
由於在Swift語言中類自己就是引用類型,因此在設計Order時,咱們值須要將其中的customer字段改爲引用外部的Customer類的對象便可。這樣一來多個訂單能夠引用同一個用戶了,並且一個訂單對用戶信息修改後,其餘訂單的用戶信息也會隨之改變。要實現這一點須要對Order的構造函數和customer的設置函數進行修改,將在Order內部建立Customer對象的方式改變成將外部Customer對象的引用賦值給Order中的custom對象。說白了,修改後的Order中的customer對象就是外部對象的一個引用。這種方法能夠將值對象改變成引用對象
上面這種作法能夠將值對象改變成引用對象,可是代價就是改變Order建立的方式。上面代碼修改完了,那麼咱們的測試用例也就做廢了,由於Order的建立方式進行了修改,須要外部傳入一個Customer對象,下方截圖就是咱們修改後的測試用例。(若是你是在你的工程中這麼去將值對象修改引用對象的,不建議這麼作,下面會給出比較好的解決方案)。
3.從根本上進行重構
上面代碼的修改不能稱爲代碼的重構,由於其改變的是不只僅是模塊內部的結構,並且修改了模塊的調用方式。也就是說裏外都被修改了,這與咱們重構所提倡的「改變模塊內部結構,而不改變對外調用方式」所相悖。因此在代碼重構時不要這麼作了,由於上面的這種作法的成本會很高,而且出現BUG的概率也會提升。由於每一個使用訂單的地方都會建立一個Customer的類來支持訂單的建立,那麼問題來了,若是同一用戶在不一樣地方建立訂單怎麼辦?因此上面的作法仍是有問題的,終歸是治標不治本。因此咱們要從根本上來解決這個問題。由於該問題是由於Customer數據不一樣步引發的,因此咱們還得從Customer來下手。
該部分的重構,在第一部分的基礎上作起。咱們本次重構的目標就是「不改變對外調用方式,但能保持每一個用戶是惟一的」。好接下來就開始咱們真正的重構工做。在本次重構中,依照重構的規則,咱們不會去修改咱們的測試用例,這一點很重要。
(1)從根本解決問題,首先咱們對Customer進行重構。在Customer中添加了一個靜態的私有變量customers, 該靜態私有變量是字典類型。其中存儲的就是每次建立的消費者信息。在字典中每一個消費者的key爲消費者獨一無二的身份證信息(idCard)。在添加完上述變量後,咱們須要爲建立一個工廠方法createCustomer() 在工廠方法中,若是當前傳入的用戶信息未被存入到字典中,咱們就對其進行建立存入字典,並返回該用戶信息。若是傳入的用戶已經被建立過,那麼就從字典中直接取出用戶對象並返回。具體作法以下所示。
(2)、對Customer類修改完畢後,咱們須要在Order中經過Customer的工廠方法來獲取Customer類的實例,這樣就能保證Order中的customer對象也是引用對象了。不過此時的引用對象是從Customer中獲取的,而不是外部傳過來的。下方是Order類中對工廠方法的調用,這樣作的好處就是,咱們只對模塊的內部進行了修改,而測試用例無需修改。
(3)、對這次重進行測試,咱們任然使用第一部分使用的測試用例。也就是說該模塊對外的接口是沒有變化的,下方就是對重構後的代碼的測試結果。由結果能夠看出,在不一樣訂單中的用戶,只要是信息一致,那麼其內存地址是一致的。也就是通過重構,咱們將原來的值對象改爲了引用對象。
4、Change Reference to Value(將引用對象改成值對象)
將引用對象改成值對象,該重構規則正好與上面相反。在一些狀況下使用值對象更爲簡單,更易管理,但前提是該值對象很小而且不會被改變。在這種狀況下你就沒有必要使用引用對象了。從上面的示例來看,使用引用對象實現起來仍是較爲複雜的。仍是那句話,若是你的對象很是小,並且在建立後其中的數據不會被改變,若是須要改變就必須在建立一個新的對象來替換原來的對象。在這種狀況下使用值對象是徹底能夠的。在此就不作過多的贅述了。
不過在使用值對象時,你最好爲值對象提供一個重載的相等運算符用來比較值對象中的值。也就是說只要是值對象中的每一個屬性的值都相同,那麼這兩個值對象就相等。至於如何對「==」 運算符進行重載就不作過多的贅述了,由於該知識點不是本篇博客的重點。
5、Replace Array or Dictionary with Object(以對象取代數組或字典)
這一點呢和本篇博客的第二部分實際上是一個。就是當你使用數組或者字典來組織數據,這些數據組合起來表明必定的意義,這是最好將其定義成一個實體類。仍是那句話,定義成實體類後,數據更易管理, 便於後期需求的迭代。下方代碼段就是講相應的字典和數組封裝成一個實體類,由於確實比較簡單,在此就不作過多的贅述了。具體請參加下方代碼段。
6、Duplicate Observed Data(複製「被監測數據」)
這一部分是比較重要的部分,也是在作UI開發時常常遇到的部分。用大白話將就是你的業務邏輯與GUI柔和在了一塊兒,由於UI做爲數據的入口,因此在寫程序時,咱們就很容易將數據處理的方式與UI寫在一塊兒。這樣作是很是很差的,不利於代碼的維護,也不利於代碼的可讀性。隨着需求不斷的迭代,版本不斷的更新,UI與業務邏輯融合的代碼會變得很是難於維護。因此咱們仍是有必要將於UI無關的代碼從UI中進行分離,關於如何進行分層宏觀的作法請參加以前發佈的博客《iOS開發之淺談MVVM的架構設計與團隊協做》。
今天博客中的該部分是分層的微觀的東西,也就是具體如何將業務邏輯從GUI中進行剝離。因此在接下來的實例中是和UI實現有關的,會根據一個比較簡單的Demo來一步步的將UI中的業務邏輯進行分離。進入該部分的主題。複製「被監測數據」簡單的說,就是將UI提供的數據複製一份到咱們的業務邏輯層,而後與UI相應的數據進行關聯,UI數據變化,被複制的業務邏輯中的數據也會隨之變化。這一點也就是所謂的"響應式編程"吧,關於響應式編程,iOS開發中會常常用到ReactiveCocoa這個框架,關於ReactiveCocoa的內容,請參見以前的博客《iOS開發之ReactiveCocoa下的MVVM》。今天的示例中,使用了一個比較簡單的方式來同步這些數據,使用了"事件監聽機制"。下方就建立一個比較簡單的Demo。
1.建立示例
要建立的示例比較簡單,在UI方面,只有三個輸入框用來接收加數與被加數,以及用來顯示兩數之和。而後使用兩個UILabel來顯示+號與=號。咱們要實現的功能就是改變其中一個加數與被加數時,自動計算兩個數的和並顯示。
要實現上述功能的代碼也是比較簡單的,總共沒有幾行,下方這個類就是實現該功能的所有代碼。代碼的核心功能就是「獲取加數與被加數的和,而後在加數與被加數的值有一個改變時,就會計算二者之和,並將和賦值給最後一個輸入框進行顯示」。具體代碼以下所示。
1 class AddViewControllerBack: UIViewController { 2 3 //三個輸入框對應的字段 4 @IBOutlet var firstNumberTextField: UITextField! 5 @IBOutlet var secondNumberTextField: UITextField! 6 @IBOutlet var resultTextField: UITextField! 7 8 override func viewDidLoad() { 9 super.viewDidLoad() 10 } 11 12 //獲取第一個輸入框的值 13 func getFirstNumber() -> String { 14 return firstNumberTextField.text! 15 } 16 17 //獲取第二個輸入框的值 18 func getSecondNumber() -> String { 19 return secondNumberTextField.text! 20 } 21 22 //加數與被加數中的值改變時會調用的方法 23 @IBAction func textFieldChange(sender: AnyObject) { 24 self.resultTextField.text = calculate(getFirstNumber(), second: getSecondNumber()) 25 } 26 27 28 //計算兩個數的值 29 func calculate(first: String, second: String) -> String { 30 return String(stringToInt(first) + stringToInt(second)) 31 } 32 33 //將字符串安全的轉變成整數的函數 34 func stringToInt(str: String) -> Int { 35 guard let result = Int(str) else { 36 return 0 37 } 38 return result 39 } 40 }
2.對上述代碼進行分析並重構
由於代碼比較簡單,因此很容易進行分析。在上述UI代碼中,咱們很清楚的看到後兩個函數,也就是calculate()與stringToInt()函數是數據處理的部分,只依賴於數據,與UI關係不是很大,因此咱們能夠使用複製「被監測數據」規則將該段業務邏輯代碼進行提取重構。重構後UI以及UI對外的工做方式不變。
下方的Calculate類就是咱們提取的數據業務類,負責處理數據。在該類中咱們建立了三個屬性來與UI中的輸入框進行對應,這也就是所說的複製「被監測的數據」。由於和也就是resultNumber是由firstNumber和SecondNumber計算而來的,因此咱們就把resultNumber定義成了計算屬性,而firstNumber和secondNumber爲存儲屬性。併爲存儲屬性提供setter方法。在Calculate類的構造函數中,咱們爲兩個值指定了初始化數據也就是「0」。最下方的那兩個函數就是咱們從UI中直接拷貝過來的數據,一點沒有修改,也是能夠工做的,由於這部分代碼只依賴於數據,而不依賴於UI。
建立爲相應的業務邏輯處理類並提取完業務邏輯後,咱們須要將業務邏輯中的數據,也就是複製過來的數據與UI中的數據提供者進行綁定,並返回計算結果。下方紅框中就是咱們要修改的部分,在UI中咱們刪除掉處理業務數據的代碼,而後建立也給Calculate對象,並在相應的事件監聽的方法中更新Calculate對象中的數據。以下所示
7、Change Unidirectional Association to Bidirectional(將單向關聯改成雙向關聯)
要介紹本部分呢,我想引用本篇博文中第(三)部分是實例。由於在第三部分的實例中Customer與Order的關係是單向關聯的,也就是說Order引用了Customer, 而Customer沒有引用Order。換句話說,咱們知道這個訂單是誰的,但你不知道只經過用戶你是沒法知道他有多少訂單的。爲了只經過用戶咱們就能知道該用戶有多少訂單,那麼咱們須要使用到「將單向關聯改成雙向關聯」這條規則。
1. 在Customer類中添加上指向Order類的鏈
由於Customer沒有指向Order類的鏈,因此咱們不能獲取到該用戶有多少訂單,如今咱們就要添加上這條鏈。將單向關聯改成雙向關聯,具體作法是在Customer中添加一個數組,該數組中存儲的就是該用戶所擁有的訂單。這個數組就是咱們添加的鏈。數組以下:
1 //添加與Order關聯的鏈,一個用戶有多個訂單 2 private var orders:Array<Order> = []
在Customer中值只添加數組也是不行的呢,根據以前提到的重構規則,咱們要爲數組封裝相應的操做方法的,下方就是咱們要在Customer中添加的操做數組的方法。具體代碼以下所示:
1 //====================添加================== 2 func addOrder(order: Order) { 3 self.orders.append(order) 4 } 5 6 func getOrders() -> Array<Order> { 7 return self.orders 8 }
在Order類關聯Customer時,創建Customer到Order的關聯。也就是將當前訂單添加進該用戶對應的訂單數組中,具體作法以下:
與之對應的規則是Change Bidirectional Association to Unidirectional(將雙向關聯改成單向關聯),就是根據特定需求刪去一個鏈。就是說,原來須要雙向鏈,可現在因爲需求變動單向關聯便可,那麼你就應該將雙向關聯改成單向關聯。
8、Replace Magic Number with Synbolic Constant(以字面常量取代魔法數)
這一點說白了就是不要在你的應用程序中直接出現字數值。這一點很好理解,在使用字面數值時,咱們要使用定義好的常量來定義。由於這樣更易於維護,若是同一個字面數值寫的處處都是,維護起來及其困難。當使用字面常量時維護起來就容易許多。該規則比較容易理解,在此不作過多的贅述。看下方實例便可。對於下方的實例而言,若是在版本迭代中所需的PI的精度有所改變,那麼對於替換後的程序而言,咱們只需修改這個常量的值便可。
1 func test(height: Double) -> Double { 2 return 3.141592654 * height 3 } 4 5 //替換 6 let PI = 3.141592654 7 func test1(height: Double) -> Double { 8 return PI * height 9 }
9、Encapsulate Field(封裝字段)
當你的類中有對外開放字段時,最好將其進行封裝,不要直接使用對象來訪問該字段,該優缺點與上述的「自封裝字段」的優缺點相似。由於直接訪問類的字段,會下降程序的模塊化,不利於程序的擴充和功能的添加。再者封裝是面向對象的特徵之一,因此咱們須要將字段變成私有的,而後對外提供相應的setter和getter方法。具體作法以下所示。
1 //重構前 2 class Person { 3 var name: String = "" 4 5 init(name: String) { 6 self.name = name 7 } 8 } 9 10 //重構後 11 class Person { 12 private var name: String = "" 13 14 init(name: String) { 15 self.name = name 16 } 17 18 func getName() -> String { 19 return name 20 } 21 22 func setName(name: String) { 23 self.name = "China:" + name 24 } 25 }
10、Encapsulate Collection(封裝集合)
「封裝集合」這一重構規則應該來講並不難理解。當你的類中有集合時,爲了對該集合進行封裝,你須要爲集合建立相應的操做方法,例如增刪改查等等。下方就經過一個不封裝集合的實例,看一下缺點。而後將其重構。關於「封裝集合」具體的細節參見下方實例。
1.未封裝集合的實例
下方咱們先建立一個圖書館圖書類,爲了簡化示例,該圖書類只有一個書名。下方代碼段就是這個圖書類,以下所示:
class LibraryBook { private var name: String init(name: String) { self.name = name } func getName() -> String { return self.name } }
緊接着要建立一個借書者,借書者中有兩個字段,一個是借書者的名字,另外一個是所借書籍的數組。在Lender中咱們沒有爲lendBooks數組封裝相應的方法,只爲其提供了getter/setter方法,具體代碼以下所示。
1 class Lender { 2 private var name: String 3 private var lendBooks: Array<LibraryBook> = [] 4 5 init(name: String) { 6 self.name = name 7 } 8 9 func getName() -> String { 10 return self.name 11 } 12 13 func setLendBooks(books: Array<LibraryBook>) { 14 self.lendBooks = books 15 } 16 17 func getLendBooks() -> Array<LibraryBook> { 18 return self.lendBooks 19 } 20 }
緊接着咱們要建立一個測試用例,觀察這兩個類的使用方式。由下面程序的註釋可知,首先咱們須要建立一個books的數組,該數組就像一個籃子似的,它能夠存儲咱們要借的書籍。讓後將建立的書籍添加到該數組中,最後將books賦值給借書人中的lendBooks。若是要對書籍進行修改,那麼只有先獲取借書人的lendBooks, 而後進行修改,最後再將修改後的值賦值回去。
1 //先建立一個書籍數組 2 var books: Array<LibraryBook> = [] 3 //添加要借的書籍 4 books.append(LibraryBook(name: "《雪碧加鹽》")) 5 books.append(LibraryBook(name: "《格林童話》")) 6 books.append(LibraryBook(name: "《智慧意林》")) 7 8 //建立借書人 9 let lender: Lender = Lender(name: "ZeluLi") 10 lender.setLendBooks(books) 11 12 //獲取所借書籍 13 var myBooks = lender.getLendBooks() 14 15 //對書籍數組修改後再賦值回去 16 myBooks.removeFirst() 17 lender.setLendBooks(myBooks)
2.爲上面的Lender類添加相應的集合操做的方法
由上面的測試用例能夠看出,Lender類封裝的很差。由於其使用方式以及調用流程太麻煩,因此咱們得從新對其進行封裝。因此就會用到「Encapsulate Collection」原則。下面咱們就會爲Lender添加上相應的集合操做的方法。說白了,就是講上面測試用例作的一部分工做放到Lender類中。下方是爲Lender添加的對lendBooks相應的操做方法。下方代碼中的Lender類與上面的Lender類中的lendBooks不一樣,咱們使用了另外一個集合類型,也就是字典,而字典的key就是書名,字典的值就是書的對象。具體代碼以下所示:
通過上面這樣一封裝的話,使用起來就更爲合理與順手了。用大白話講,就是好用。下方是咱們從新封裝後的測試用例,簡單了很多,並且組織也更爲合理。具體請看下方代碼段:
11、Replace Subclass with Fields(以字段取代子類)
什麼叫「以字段取代子類」呢?就是當你的各個子類中惟一的差異只在「返回常量數據」的函數上。當遇到這種狀況時,你就能夠將這個返回的數據放到父類中,並在父類中建立相應的工廠方法,而後將子類刪除便可。直接這樣說也許有些抽象,接下來,咱們會經過一個小的Demo來看一下這個規則具體如何應用。1.建立多個子類,並每一個子類只有一個函數的返回值不一樣
接下來咱們就要建立重構前的代碼了。首先咱們建立一個PersonType協議(也就是一個抽象類),該協議有兩個方法,一個是isMale(),若是是子類是男性就返回true,若是子類是女性就返回false。還有一個是getCode()函數,若是子類是男性就返回「M」,若是是子類是女性就返回「F」。 這兩個子類的差異就在於各個函數返回的值不一樣。下方是PersonType的具體代碼。
1 protocol PersonType { 2 func isMale() -> Bool 3 func getCode() -> String 4 }
而後咱們基於PersonType建立兩個子類,一個是Male表示男性,一個是Female表示女性。具體代碼以下:
1 class Male: PersonType { 2 func isMale() -> Bool { 3 return true 4 } 5 6 func getCode() -> String { 7 return SenderCode.Male.rawValue 8 } 9 } 10 11 class Female: PersonType { 12 func isMale() -> Bool { 13 return false 14 } 15 16 func getCode() -> String { 17 return SenderCode.Female.rawValue 18 } 19 }
上述代碼的SenderCode是咱們自定義的枚舉類型,用來表示"M"與「F」,枚舉的代碼以下:
1 enum SenderCode: String { 2 case Male = "M" 3 case Female = "F" 4 }
2.以字段取代子類
從上面的代碼容易看出,Male與Female類實現相同的接口,但接口函數在兩個類中的返回值是不一樣的。這時候咱們就能夠使用「以字段取代子類」的方式來進行重構,下方截圖就是重構後的代碼片斷。
下方代碼中,將PersonType聲明瞭一個類,在類中添加了兩個字段,一個是isMale,另外一個是code,這兩個字段剛好是上述兩個子類函數中返回的不一樣值。這也就是使用字段來取代子類,由於有了這兩個字段,咱們就能夠不用去建立子類了,而是直接在PersonType中經過工廠方法根據不一樣的性別分別給這兩個新加的字段賦上不一樣的值。具體作法以下。
通過上面這段代碼重構後,咱們就能夠調用PersonType的不一樣的工廠方法來建立不一樣的性別了。測試用例以下所示:
繼續更新有關重構的博客,前三篇是關於類、函數和數據的重構的博客,內容還算比較充實吧。今天繼續更新,本篇博客的主題是關於條件表達式的重構規則。有時候在實現比較複雜的業務邏輯時,各類條件各類嵌套。若是處理很差的話,代碼看上去會很是的糟糕,並且業務邏輯看上去會很是混亂。今天就經過一些重構規則來對條件表達式進行重構,讓業務邏輯更爲清晰,代碼更以維護和擴展。
今天博客中的代碼示例依然是Swift班,在對條件表達式重構時也會提現出Swift的優雅之處,會用上Swift特有的語法及其特色,好比使用guard來取代if-let語句等。若是你的需求的業務邏輯及其複雜,那麼妥善處理條件表達式尤其重要。由於對其妥善處理能夠提升代碼的可讀性,以及提升代碼的可維護性。說這麼多仍是來些示例來的直觀,下方會根據一些Demo來着重分享一些條件表達式的部分重構規則,固然今天博客中沒有涵蓋全部的條件表達式的重構規則,更詳細的部分請參見經典的重構書籍。
今天所分享的代碼段也將會在github上進行分享,分享地址在本篇博文的後方。廢話少說了,進入今天的主題。
一.Decompose Conditional(分解條件表達式)
顧名思義,分解條件表達式說白了,就是當你的條件表達式比較複雜時,你就能夠對其進行拆分。通常拆分的規則爲:經if後的複雜條件表達式進行提取,將其封裝成函數。若是if與else語句塊中的內容比較複雜,那麼就將其提取,也封裝成獨立的函數,而後在相應的地方進行替換。
下方代碼段就是咱們將要重構的代碼段。由於本篇博客的主題是對條件表達式的重構,因此咱們要對象下方的if-else的代碼塊進行重構。至於下方代碼片斷中其餘不規範以及須要重構的地方咱們暫且忽略。由於咱們本篇博客的主題是條件表達式的重構。接下來咱們就要對下方代碼片斷中的條件表達式進行分析了。由於下方這段代碼畢竟是一個Demo,在這兒咱們能夠作個假設,假設if後邊的表達式比較複雜,而後在if語句塊和else語句塊中都有一些複雜的處理,代碼看上去的大致樣子以下所示。
基於對上述代碼的結構的假設,接下來咱們將要對其進行重構。說白了,就是讓將條件表達式中的比較複雜的模塊進行拆分與提取。下方代碼段就是咱們重構後的結構,就是將咱們假設比較複雜的模塊進行封裝,而後在條件表達式中使用函數進行替換。這樣的話,在看條件表達式就比較清晰。固然,咱們這個Demo的條件表達式不夠複雜,而且if和else的邏輯塊所作的東西很少。不過咱們能夠假設一下,若是在比較複雜的狀況下,這種重構手法是比較實用的。具體的你們就看重構前與重構後的區別吧。
2、Consolidate Conditional Expression(合併條件表達式)
「合併條件表達式」這條規則也是比較好理解的,由於有時候會存在這樣的狀況,也就是一些條件表達式後的語句體執行的代碼塊相同。說白了也就是不一樣的條件有着一樣的返回結果。固然通常在你程序設計之初不會出現此問題,由於在咱們設計程序時,若是不一樣的條件返回相同的結果,咱們確定會將其合併的。不過當你在多個版本迭代,多個需求要增長,或者在別人的代碼上進行需求迭代的時候,該狀況是頗有可能發生的。
說這麼多,也許有些抽象,那麼就直接看下方須要重構的Demo了。固然,下方的Demo中,咱們爲了測試,其中的條件比較簡單。咱們假設每一個條件表達式是在不一樣的需求迭代中或者修改bug時添加的,從而形成了下方這種狀況(固然下方的狀況有些誇張,這也是爲了突出要合併條件的狀況)。
在上述誇張的Demo中一眼就能看出來如何進行重構了(在平常開發迭代中,由於業務邏輯的複雜性或者屢次迭代的緣由,每每不是那麼一目瞭然)。接下來咱們就要對不一樣條件,但返回相同結果的部分進行合併。下方就是咱們合併後的結果,重構手法就是講不一樣的條件表達式使用&&或者||等布爾運算進行合併。
合併後,若是條件比較複雜,那麼咱們就能夠使用本片博客中的第一部分使用的重構規則進行再次重構。下方代碼段是進行第二次重構,就是對比較複雜的表達式進行函數封裝,具體以下所示。仍是那句話,Demo有些誇張,不過用來演示該重構規則也是不錯的,思想就這個思想,具體在平常開發中的使用場景仍是須要進行琢磨和推敲的。
3、Consolidate Duplicate Conditional Fragments(合併重複的條件片斷)
第二部分合並的是條件表達式,本部分是合併的是重複的條件片斷。什麼叫合併重複的條件片斷呢?這種狀況也是通常不會在設計程序之初所出現,可是隨着時間的推移,項目不斷迭代更新,或者需求變動和迭代更新等等,在項目後期維護時比較容易出現重複的條件片斷。在開發中是比較忌諱重複的代碼的,若是出現重複的代碼,那麼說明你的代碼應該被重構了。
下方代碼片斷中if與else中有着相同的語句,就是這個print語句。固然這個示例也是比較誇張的,可是足以說明問題。若是你在開發業務邏輯比較複雜的條件表達式時,要謹慎的檢查一下有沒有下方這種狀況。也就是出現了重複的條件片斷。這種狀況在需求迭代或者變動中是及其容易出現的。固然下方只是咱們這兒列舉的一個誇張的示例。
對於這個示例而言,咱們不難看出,去代碼的重複化。將print語句移到條件以外。可是要學會觸類旁通呢,重要的是重構手法和思想。在真正的項目中,若是你要提取重複的代碼段通常還要結合着其餘重構手法,好比將重複的部分先提取成一個獨立的模塊(獨立的類或者方法),而後在條件中使用,最後再去重複話。這樣一來,重構的思路就比較清晰了。雖然今天的示例比較簡單,可是足以表達這個思路。下方是重構後的代碼。若是你對下方代碼看着不爽的話,徹底能夠根據以前咱們介紹的重構手法「使用查詢來替代臨時變量」,將下方的代碼繼續重構,在本章博客中就不作過多介紹了。
4、Remove Control Flag(移除控制標記)
「移除控制標記」這一點仍是比較重要的,我平時在代碼開發中有時候也會使用到標記變量,來標記一些事物的狀態。使用標記變量最直觀的感覺就是不易維護,不易理解。由於在需求變動或者迭代中,你還得維護這標記變量。若是維護一個標記變量簡單的話,那麼維護多個標記變量就沒這麼容易了。並且在你的程序中使用標記變量時,不易理解,而且會顯得邏輯混亂。固然這是個人直觀感覺,在寫程序時,我儘可能會避免使用標記變量。
固然,下方又是一個有點誇張的例子,可是該例子能夠說明問題。下方代碼中咱們使用了一個flag標記變量,固然下方代碼沒有什麼意義了。在平時開發中咱們會使用一些標記變量來標記一個或者一些數據的狀態,或者一些控件的狀態,再次爲了簡化示例,咱們就簡單的引入了一個flag標記變量。下方代碼不難理解,當i爲20時,咱們就翻轉標記變量的狀態,而後if中的語句塊就不被執行了。
雖然下方代碼片斷是我寫的,可是我我的看着超級的不舒服。引入的這個flag增長了代碼的邏輯複雜度,讓代碼變得不那麼直觀。我我的建議,在平時開發中儘可能的要少使用標記變量。不到萬不得已,不要在你的代碼中引入標記變量。若是有,嘗試着去除標記變量。
標記變量通常是能夠使用其餘語句進行替換的,能夠使用break、return、continue等等,這個要根據具體狀況而定。總之,代碼中有標記變量不是什麼好的事情。下方代碼段就是對上述代碼去除標記變量的重構。重構後的代碼以下所示,固然還有好多其餘去除的方法,此處僅僅給出了一種。
5、Replace Nested Condition with Guard Clauses(以衛語句取代嵌套的條件)
條件表達式的嵌套是使人討厭的東西。代碼中有多層if-else嵌套會下降代碼的可讀性以及可維護性,若是此時在加上for循環等等其餘邏輯語句,想一想均可怕。這種業務邏輯較強的代碼要慎重對待。儘可能不要將if-else進行嵌套,由於嵌套的if-else確實很差理解,若是在出現bug時,更是很差定位bug。要記住,你寫的代碼不是給機器看的,而是給人看的,這一點很是重要。不光是代碼編寫規範,也儘可能不要使用理解起來比較費勁的語句來實現你的邏輯。
下方咱們將建立一種場景,人爲的建立多個if嵌套的狀況。下方的demo理解起來應該不難,第一個數組中存儲的是第二個字典的key,第二個字典中存儲的value是下一個字典也就是第三個字典的key,以此類推。將咱們在使用從相應的字典中取出的value作爲key再次取值時,咱們要保證該值不爲nil,因此咱們要進行if-let判斷。if-let所表示的意思是在取值時,若是當前取出的值不爲nil,那麼就執行if後的語句體,若是爲nil,那麼就不執行。這樣一來,就會出現多層if-let嵌套的狀況。
固然,在一些業務邏輯比較複雜的需求中,嵌套的每層if後都跟着不一樣的表達式,而不只僅是if-let。由於爲了建立這個if嵌套的場景,再次咱們使用了if-let嵌套。這麼多的if-let嵌套顯然不是什麼好的事情,因此咱們要對此重構。
若是多層if嵌套,會出現一種叫作「厄運金字塔」的現象,由於在if左邊會出現一個三角號的空間。這可不是什麼好的標誌,這樣的代碼結構通常理解起來會比較困難,維護起來也不是那麼的理想。因此下方咱們要對上述代碼進行結構。要去除上面的嵌套模式,咱們能夠將if後的條件進行翻轉,根據具體需求再引入return、break、continue等衛語句。下方是講條件進行翻轉而後引入了continue語句,代碼以下:
該部分的第二段代碼要比第一段代碼容易理解的多。通過條件翻轉+continue,將上述嵌套的條件語句進行了拆分。拆分紅了三個獨立的if語句,雖然代碼結構不一樣,可是其實現功能都是同樣的。不過上面的解決方案在Swift中並不完美。由於Swift語言是很是優雅的,Swift語言在設計的時候就考慮到了這種狀況,因此在Swift 2.0時推出了guard語句。在這種狀況下使用guard語句再合適不過了,下方代碼段就是使用guard語句進行了重構。
使用guard let聲明的變量與guard自己同在一個做用域,也就是說下方代碼在guard let中聲明的變量能夠在for循環中直接使用。guard語句的用法就是若是guard 後方的賦值語句所取出的值爲nil,那麼就會執行else中的語句,不然就會繼續往下執行。在else中通常是break、return、continue等衛語句。這種語法形式很好的對上述糟糕的形式進行了解決,並且還易於理解。
6、Replace Condition with Polymorphism(以多態取代條件表達式)
在介紹「以多態取代條件表達式」以前呢,首先要理解面向對象中多態是什麼,也就是說多態是幹嗎的。顧明思議,多態就是類的不一樣類型的對象有着不一樣的行爲狀態。若是在你的條件表達式中條件是對象的類型,也就是根據對象的不一樣類型而後作不一樣的事情。在這種狀況下使用多態在合適不過了。若是該部分在設計模式中,應該對應着狀態模式這一部分。這就是以多態來取代條件表達式。
下方是一個比較簡單的示例,這也正是咱們要進行重構的示例。在Book類中有三中類型,也就是咱們的書有三種,具體每種書是什麼這不是該示例的重點。在Book類實例化時,須要爲書的對象指定該書的類型(三種類型中的一種)。在Book類中,還有一個核心方法,那就是計算書的價格。在charge()函數中,根據不一樣的書的種類,給出了不一樣的價格。固然在Switch中的分支的計算方法在本例中很是簡單,可是咱們要假設每一個分支的計算很是複雜,並且有着多行代碼。
在這種假設的狀況下,下方的條件語句是很是糟糕的,由於龐大的業務邏輯增長了代碼維護的成本。在這種狀況下咱們就能夠使用多態來取代複雜的條件表達式。
若是想使用多態,引入其餘類是必不可少的,並且每一個類中也必須有相應的對應關係。「以多態取代條件表達式」的作法的本質是將不一樣狀態的業務邏輯的處理的代碼移到相應的類中。在本示例中,咱們要建立三種書籍的價格類,而且將上述case中的「複雜」計算移入到相應的書籍類中。由於每一個書籍價格中都會有相應的計算方法,也就是charge()方法,因此咱們爲這三個書籍價格定義了一個協議(接口或者抽象類),在協議中就給出了charge()函數。而後咱們就能夠將不一樣種類的書籍實現該協議,在相應的方法中給出價格計算的代碼。具體作法以下所示:
引入上述幾個類後,在咱們的Book中就能夠使用多態了。在Book類中添加了一個price字段,這個字段的類型就是咱們的Price協議。也就是隻要是符合咱們的Price協議的對象均可以。而後在Book中也添加了一個charge()方法,在Book中的charge方法作的一件事情就是調用price對象的charge方法。關鍵的是根據不一樣的書籍類型建立不一樣的書籍價格對象。這樣一來,咱們就把每一個分支中的業務邏輯進行了分離,並使用了多態來獲取價格。重構後的優勢不言而喻。
陸陸續續的發表了多篇關於重構的文章了,仍是那句話,重構是一個項目迭代開發中必不可少的一個階段。其實重構伴隨着你的項目的整個階段。在前幾篇關於重構的文章中咱們談到了函數的重構、類的重構、數據的重構以及條件表達式的重構,那麼今天我們就來聊聊繼承關係的重構。固然仍是延續前幾篇博客的風格,咱們在博客中的代碼實例依然使用Swift語言來實現,固然仍是那句話,使用什麼語言無所謂,關鍵是看重構的場景以及重構的思想。
「重構」不只僅能夠改善你既有的代碼設計,還能夠改變你組織代碼的思路,使你的程序在設計之初就趨於合理化,利於程序的擴充。重構每每伴隨着設計模式的使用,在重構系列的博客結束後,我想系統的給你們分享一下關於設計模式的東西。固然是結合着各類實例。所謂一名Coder,重構和設計模式是必須涉獵的部分,由於這二者可讓你寫出更漂亮的代碼,固然要想真正的掌握設計模式以及各類重構手法,還得結合不一樣的實例來進行實踐。理論當然重要,可是要想將理論的東西變成你本身的,還必須將理論付諸實踐。廢話少說,進入今天的主題。
一.Pull Up Field (字段上移) & Pull Down Field (字段下移)
字段上移與字段下移是相對的,也是咱們以前所說的「凡事都有其兩面性」,咱們要辯證的去看待。咱們只對Pull Up Field (字段上移) 這個規則作討論,那麼關於Pull Down Field (字段下移)咱們不作過多的討論,由於這兩條規則是相反的,理解一條後,把這條規則反過來就是咱們要理解的另外一條規則。這樣提及來,仍是比「觸類旁通」要容易的多。
下方這個實例是爲了解釋「字段上移」所實現的一個Demo。固然Demo看上去不只簡單並且是有些誇張的,不過說明字段上移這個規則是徹底足夠了的。好比咱們有一個父類爲MySuperClass,咱們有一個子類SubClass1,而在SubClass1中有一個字段父類是沒有的。由於後期需求迭代或者需求變動,咱們須要再建立一個SubClass1的兄弟類,就是下方的SubClass2。在SubClass2中與SubClass1中存在相同的字段,那就是var a = 0。
在上述狀況下,就須要使用到咱們的「字段上移」的規則。也就是說將子類中相同的字段移到父類中。在該實例中就是講var a = 0 移到父類中。重構後的代碼以下所示:
而將「Pull Down Field (字段下移)」正好與上面的狀況相反。也就是父類中有某些字段,可是這些字段只有在少數子類中使用到,在這種狀況下咱們須要將這個字段移到相應的子類中便可。除了Pull Up Field (字段上移) & Pull Down Field (字段下移) 這兩個規則外,Pull Up Method (將函數上移) 和 Pull Down Method (將函數下移)這兩個規則與上述狀況相似。就是將上面的字段改爲函數,有時候不只字段會出現上述狀況,函數也會出現上述狀況,須要咱們進行移動。由於使用場景相似,再次就不作過多的贅述了。
2、Extract Subclass (提煉子類)
這種狀況下用的仍是比較多的,當類中的某些方法只有在特定的類的實例中才會使用到,此時咱們就須要提煉出一個子類,將該方法放到相應的子類中。這樣一來咱們的每一個類的職責更爲單一,這也就是咱們常說的「單一職責」。
在下方示例中,CustomerBook是一個圖書消費者的類。其中customeCharge()方法是普通用戶計算消費金額所需的方法,而vipCharge()方法是VIP用戶調用的方法,在內部vipCharge()須要調用customeCharege()方法。可是對外部而言,vipCharge()方法只有VIP用戶纔會用到,在這種狀況下咱們就須要使用「Extract Subclass (提煉子類)」規則對VIP進行提煉。
具體作法是咱們須要提煉出一個子類,也就是說將VIP用戶做爲普通用戶的子類,而後將只有VIP用戶才調用的方法放到咱們的VIP子類中。這樣一來層次更加明確,每一個類的職責更爲單一。上述示例重構後的結果以下所示。
與「提煉子類」規則相對應的是「Collapse Hierarchy (摺疊繼承關係)」。一句話來歸納:就是當你的父類與子類差異不大時,咱們就能夠將子類與父類進行合併。將上面的示例翻轉就是「Collapse Hierarchy (摺疊繼承關係)」規則的示例,再次就不作過多的贅述了。
3、Form Template Method (構造模板函數)
Form Template Method (構造模板函數)這一規則仍是比較實用的。先說模板,「模板」其實就是框架,沒有具體的實現細節,只有固定不變的步驟,能夠說模板不關心具體的細節。舉個栗子🌰,像前段時間比較火的「祕密花園」,那些沒有顏色的線條就是模板,若是一些人獲取的是同一本祕密花園,那麼說明每一個人所獲取的模板是相同的。可是每一個人對每塊的區域所圖的顏色又有差別,這就是實現細節的不一樣。
言歸正傳,當兩個兄弟類中的兩個函數中的實現步驟大體一直,可是具體細節不一樣。在這種狀況下,咱們就能夠將大致的步驟提取成模板,放到父類中,而具體細節由各自的子類來實現。具體實現請看下方的類,在Subclass1和Subclass2中的calculate()方法中的大致步驟是相同的,就是對兩個值相加,而後返回這兩個值的和。可是具體細節不一樣,能夠看出兩個相加值的具體計算方式不一樣。
在上述狀況下咱們就能夠使用「Form Template Method (構造模板函數)」規則將相同的計算流程進行提取,也就是構造咱們的模板函數。將模板函數放到兩個類的父類中,而後在相應的子類中只給出實現細節便可。下方代碼段是重構後的代碼,父類中多出的方法就是咱們提取的模板函數,而子類中只給出相應的實現細節便可。
4、以委託取代繼承(Replace Inheritance with Delegation)
有時候咱們爲一些類建立子類後,發現子類只使用了父類的部分方法,並且沒有繼承或者部分繼承了父類的數據。在這種狀況下咱們就能夠將這種繼承關係修改爲委託的關係。具體作法就是修改這種繼承關係,在原有子類中添加父類的對象字段,在子類中建立相應的方法,在方法中使用委託對象來調用原始父類中相應的方法。
下方示例是咱們假想出來的,可是說明該規則是綽綽有餘了。咱們假設SubClass01類中只會用到SuperClass01中的display()方法,而沒有繼承父類中的數據。在下方示例中是繼承關係,在這種狀況下咱們須要將其轉換成委託關係。
下方是咱們重構後的代碼,在下方代碼中咱們去除了以前的繼承關係。並在子類中建立了一個以前父類的代理對象,而且建立了一個相應的方法,在該新建的方法中經過代理對象來調用相應的方法。具體以下所示。
上述規則與以繼承取代委託(Replace Delegation with Inheritance)原則相對於,使用狀況與上述相反,再次就不作過多的贅述了。
不管作什麼事情呢,都要有始有終呢。前邊連續發表了5篇關於重構的博客,其中分門別類的介紹了一些重構手法。今天的這篇博客就使用一個完整的示例來總結一下以前的重構規則,也算給以前的關於重構的博客畫一個句號。今天的示例借鑑於《重構,改善既有代碼的設計》這本書中的第一章的示例,在其基礎上作了一些修改。今天博客從頭至尾就是一個完整的重構過程。首先會給出須要重構的代碼,而後對其進行分析,而後對症下藥,使用以前咱們分享的重構規則對其進行一步步的重構。
先來聊一下該示例的使用場景(若是你有重構這本書的話,能夠參加第一章中的示例,不過本博客中的示例與其有些出入)。就是一個客戶去DVD出租的商店裏進行消費,下方的程序是給店主用的,來根據用戶所借的不一樣的DVD種類和數量來計算該用戶消費的金額和積分。需求很簡單並且也不難理解。今天博客會給出原始的代碼,也是須要進行重構的代碼。固然原始代碼徹底符合需求,而且能夠正確執行。廢話少說,先看示例吧。
1、須要重構的代碼
在本篇博客的第一部分,咱們先給出完成上述需求須要重構的代碼。而後在此基礎上進行分析,使用以前咱們提到過的重構手法進行重構。首先咱們給出了電影類的實現。在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)
針對上述案例,上面測試用例的輸出結果以下。在每次重構後,咱們都會執行上述測試代碼,而後觀察結果是否與以前的相同。固然若是你的是單元測試的話,徹底能夠把對結果檢查的工做交給單元測試中的斷言來作。
2、重構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。
3、重構2:將相應的方法移到相應的類中
通過上面的重構,咱們從statement()函數中提取了兩個方法。觀察這兩個重構後的方法咱們不難看出,這兩個封裝出來的新的方法都須要一個參數,這個參數就是Rental類的對象。也就是這兩個方法都依賴於Rental類,而對該函數所在的當前類不太感冒。出現這種狀況的緣由就是這兩個函數放錯了地方,由於這兩個函數放在Customer類中不依賴與Customer類而依賴於Rental類,那就足以說明這兩個方法應該放在Rental類中。
通過咱們簡單的分析後,咱們就能夠決定要將咱們新提取的方法放到Rental類中,而且函數的參數去掉。由於函數在Rental類中,因此在函數中直接使用self便可。將計算金額的方法和計算積分的方法移到Rental類中後,咱們的Rental類以下所示。在咱們的Customer中的statement()方法中在計算金額和計算積分時,直接調用Rental中的方法便可。通過這一步重構後,不要忘記執行一下你的測試用例,監測一下重構的結果是否正確。
4、使用「以查詢取代臨時變量」再次對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中新添加的方法的調用。
通過上面的重構,咱們的方法彷佛是找到了歸宿了。重構就是這樣,一步步來,不要着急,沒動一步老是要向着好的方向發展。若是你從第一部分中的代碼重構到第五部分,彷佛有些困難。通過上面這些間接的過程,感受也是挺愉快的蠻。下方是通過咱們此次重構的類圖。
6、使用「多態」取代條件表達式
在咱們以前的博客中對條件表達式進行重構時,提到了使用類的多態對條件表達式進行重構。接下來咱們就要使用該規則對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上進行分享。對每次重構的代碼都進行了系統的整理。今天博客中的代碼整理的結果以下。