繼續更新有關重構的博客,前三篇是關於類、函數和數據的重構的博客,內容還算比較充實吧。今天繼續更新,本篇博客的主題是關於條件表達式的重構規則。有時候在實現比較復雜的業務邏輯時,各種條件各種嵌套。如果處理不好的話,代碼看上去會非常的糟糕,而且業務邏輯看上去會非常混亂。今天就通過一些重構規則來對條件表達式進行重構,讓業務邏輯更為清晰,代碼更以維護和擴展。
今天博客中的代碼示例依然是Swift班,在對條件表達式重構時也會提現出Swift的優雅之處,會用上Swift特有的語法及其特點,比如使用guard來取代if-let語句等。如果你的需求的業務邏輯及其復雜,那么妥善處理條件表達式尤為重要。因為對其妥善處理可以提高代碼的可讀性,以及提高代碼的可維護性。說這么多還是來些示例來的直觀,下方會根據一些Demo來着重分享一些條件表達式的部分重構規則,當然今天博客中沒有涵蓋所有的條件表達式的重構規則,更詳細的部分請參見經典的重構書籍。
今天所分享的代碼段也將會在github上進行分享,分享地址在本篇博文的后方。廢話少說了,進入今天的主題。
一.Decompose Conditional(分解條件表達式)
顧名思義,分解條件表達式說白了,就是當你的條件表達式比較復雜時,你就可以對其進行拆分。一般拆分的規則為:經if后的復雜條件表達式進行提取,將其封裝成函數。如果if與else語句塊中的內容比較復雜,那么就將其提取,也封裝成獨立的函數,然后在相應的地方進行替換。
下方代碼段就是我們將要重構的代碼段。因為本篇博客的主題是對條件表達式的重構,所以我們要對象下方的if-else的代碼塊進行重構。至於下方代碼片段中其他不規范以及需要重構的地方我們暫且忽略。因為我們本篇博客的主題是條件表達式的重構。接下來我們就要對下方代碼片段中的條件表達式進行分析了。因為下方這段代碼畢竟是一個Demo,在這兒我們可以做個假設,假設if后邊的表達式比較復雜,然后在if語句塊和else語句塊中都有一些復雜的處理,代碼看上去的大體樣子如下所示。
基於對上述代碼的結構的假設,接下來我們將要對其進行重構。說白了,就是讓將條件表達式中的比較復雜的模塊進行拆分與提取。下方代碼段就是我們重構后的結構,就是將我們假設比較復雜的模塊進行封裝,然后在條件表達式中使用函數進行替換。這樣的話,在看條件表達式就比較清晰。當然,我們這個Demo的條件表達式不夠復雜,並且if和else的邏輯塊所做的東西不多。不過我們可以假設一下,如果在比較復雜的情況下,這種重構手法是比較實用的。具體的大家就看重構前與重構后的區別吧。
二、Consolidate Conditional Expression(合並條件表達式)
“合並條件表達式”這條規則也是比較好理解的,因為有時候會存在這樣的情況,也就是一些條件表達式后的語句體執行的代碼塊相同。說白了也就是不同的條件有着同樣的返回結果。當然一般在你程序設計之初不會出現此問題,因為在我們設計程序時,如果不同的條件返回相同的結果,我們肯定會將其合並的。不過當你在多個版本迭代,多個需求要增加,或者在別人的代碼上進行需求迭代的時候,該情況是很有可能發生的。
說這么多,也許有些抽象,那么就直接看下方需要重構的Demo了。當然,下方的Demo中,我們為了測試,其中的條件比較簡單。我們假設每個條件表達式是在不同的需求迭代中或者修改bug時添加的,從而造成了下方這種情況(當然下方的情況有些誇張,這也是為了突出要合並條件的情況)。
在上述誇張的Demo中一眼就能看出來如何進行重構了(在日常開發迭代中,因為業務邏輯的復雜性或者多次迭代的原因,往往不是那么一目了然)。接下來我們就要對不同條件,但返回相同結果的部分進行合並。下方就是我們合並后的結果,重構手法就是講不同的條件表達式使用&&或者||等布爾運算進行合並。
合並后,如果條件比較復雜,那么我們就可以使用本片博客中的第一部分使用的重構規則進行再次重構。下方代碼段是進行第二次重構,就是對比較復雜的表達式進行函數封裝,具體如下所示。還是那句話,Demo有些誇張,不過用來演示該重構規則也是不錯的,思想就這個思想,具體在日常開發中的使用場景還是需要進行琢磨和推敲的。
三、Consolidate Duplicate Conditional Fragments(合並重復的條件片段)
第二部分合並的是條件表達式,本部分是合並的是重復的條件片段。什么叫合並重復的條件片段呢?這種情況也是一般不會在設計程序之初所出現,但是隨着時間的推移,項目不斷迭代更新,或者需求變更和迭代更新等等,在項目后期維護時比較容易出現重復的條件片段。在開發中是比較忌諱重復的代碼的,如果出現重復的代碼,那么說明你的代碼應該被重構了。
下方代碼片段中if與else中有着相同的語句,就是這個print語句。當然這個示例也是比較誇張的,但是足以說明問題。如果你在開發業務邏輯比較復雜的條件表達式時,要謹慎的檢查一下有沒有下方這種情況。也就是出現了重復的條件片段。這種情況在需求迭代或者變更中是及其容易出現的。當然下方只是我們這兒列舉的一個誇張的示例。
對於這個示例而言,我們不難看出,去代碼的重復化。將print語句移到條件之外。但是要學會舉一反三呢,重要的是重構手法和思想。在真正的項目中,如果你要提取重復的代碼段一般還要結合着其他重構手法,比如將重復的部分先提取成一個獨立的模塊(獨立的類或者方法),然后在條件中使用,最后再去重復話。這樣一來,重構的思路就比較清晰了。雖然今天的示例比較簡單,但是足以表達這個思路。下方是重構后的代碼。如果你對下方代碼看着不爽的話,完全可以根據之前我們介紹的重構手法“使用查詢來替代臨時變量”,將下方的代碼繼續重構,在本章博客中就不做過多介紹了。
四、Remove Control Flag(移除控制標記)
“移除控制標記”這一點還是比較重要的,我平時在代碼開發中有時候也會使用到標記變量,來標記一些事物的狀態。使用標記變量最直觀的感受就是不易維護,不易理解。因為在需求變更或者迭代中,你還得維護這標記變量。如果維護一個標記變量簡單的話,那么維護多個標記變量就沒這么容易了。而且在你的程序中使用標記變量時,不易理解,並且會顯得邏輯混亂。當然這是我的直觀感受,在寫程序時,我盡量會避免使用標記變量。
當然,下方又是一個有點誇張的例子,但是該例子可以說明問題。下方代碼中我們使用了一個flag標記變量,當然下方代碼沒有什么意義了。在平時開發中我們會使用一些標記變量來標記一個或者一些數據的狀態,或者一些控件的狀態,再次為了簡化示例,我們就簡單的引入了一個flag標記變量。下方代碼不難理解,當i為20時,我們就翻轉標記變量的狀態,然后if中的語句塊就不被執行了。
雖然下方代碼片段是我寫的,但是我個人看着超級的不舒服。引入的這個flag增加了代碼的邏輯復雜度,讓代碼變得不那么直觀。我個人建議,在平時開發中盡量的要少使用標記變量。不到萬不得已,不要在你的代碼中引入標記變量。如果有,嘗試着去除標記變量。
標記變量一般是可以使用其他語句進行替換的,可以使用break、return、continue等等,這個要根據具體情況而定。總之,代碼中有標記變量不是什么好的事情。下方代碼段就是對上述代碼去除標記變量的重構。重構后的代碼如下所示,當然還有好多其他去除的方法,此處僅僅給出了一種。
五、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等衛語句。這種語法形式很好的對上述糟糕的形式進行了解決,而且還易於理解。
六、Replace Condition with Polymorphism(以多態取代條件表達式)
在介紹“以多態取代條件表達式”之前呢,首先要理解面向對象中多態是什么,也就是說多態是干嘛的。顧明思議,多態就是類的不同類型的對象有着不同的行為狀態。如果在你的條件表達式中條件是對象的類型,也就是根據對象的不同類型然后做不同的事情。在這種情況下使用多態在合適不過了。如果該部分在設計模式中,應該對應着狀態模式這一部分。這就是以多態來取代條件表達式。
下方是一個比較簡單的示例,這也正是我們要進行重構的示例。在Book類中有三中類型,也就是我們的書有三種,具體每種書是什么這不是該示例的重點。在Book類實例化時,需要為書的對象指定該書的類型(三種類型中的一種)。在Book類中,還有一個核心方法,那就是計算書的價格。在charge()函數中,根據不同的書的種類,給出了不同的價格。當然在Switch中的分支的計算方法在本例中非常簡單,但是我們要假設每個分支的計算非常復雜,而且有着多行代碼。
在這種假設的情況下,下方的條件語句是非常糟糕的,因為龐大的業務邏輯增加了代碼維護的成本。在這種情況下我們就可以使用多態來取代復雜的條件表達式。
如果想使用多態,引入其他類是必不可少的,而且每個類中也必須有相應的對應關系。“以多態取代條件表達式”的做法的本質是將不同狀態的業務邏輯的處理的代碼移到相應的類中。在本示例中,我們要創建三種書籍的價格類,並且將上述case中的“復雜”計算移入到相應的書籍類中。因為每個書籍價格中都會有相應的計算方法,也就是charge()方法,所以我們為這三個書籍價格定義了一個協議(接口或者抽象類),在協議中就給出了charge()函數。然后我們就可以將不同種類的書籍實現該協議,在相應的方法中給出價格計算的代碼。具體做法如下所示:
引入上述幾個類后,在我們的Book中就可以使用多態了。在Book類中添加了一個price字段,這個字段的類型就是我們的Price協議。也就是只要是符合我們的Price協議的對象都可以。然后在Book中也添加了一個charge()方法,在Book中的charge方法做的一件事情就是調用price對象的charge方法。關鍵的是根據不同的書籍類型創建不同的書籍價格對象。這樣一來,我們就把每個分支中的業務邏輯進行了分離,並使用了多態來獲取價格。重構后的優點不言而喻。
今天關於“條件表達式的重構”的規則,當然這不是全部的,只是列舉了一些常見的,而且經常使用重構規則。篇幅有限,今天的博客就先到這兒,還會繼續更新其他的重構規則。
今天博客中的Demo在github上的分享地址為:https://github.com/lizelu/CodeRefactoring-Swift