天天看點

代碼重構(一):函數重構規則

重構是項目做到一定程度後必然要做的事情。代碼重構,可以改善既有的代碼設計,增強既有工程的可擴充、可維護性。随着項目需求的不斷疊代,需求的不斷更新,我們在項目中所寫的代碼也在時時刻刻的在變化之中。在一次新的需求中,你添加了某些功能子產品,但這些功能子產品有可能在下一次需求中不在适用。或者你因為需求疊代與變更,使你原有的方法或者類變得臃腫,以及各個子產品或者層次之間耦合度增加。此時,你要考慮重構了。

重構,在《重構,改善既有代碼的設計》這本經典的書中給出了定義,大概就是:在不改變代碼對外的表現的情況下,修改代碼的内部特征。說白了,就是我們的測試用例不變,然後我們對既有的代碼的結構進行修改。重構在軟體開發中是經常遇到的,也是非常重要的。在需求疊代,debug,code review時,你都可以對你既有的代碼進行重構。

在接下來的幾篇博文中,我想與大家一塊去窺探一下代碼重構的美麗,學習一下代碼重構的一些規則。當然在每個規則中都有小的demo, 在本篇部落格以及相關内容的部落格是使用swift語言實作的。當然,和設計模式相同,重構重要的是手法和思想,和使用什麼樣的語言關系不大。經典的重構書籍中是使用java語言來實作的,如果你對php, python等其他語言比較熟悉,完全可以使用這些語言來測試一些重構手法。

一、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規則進行重構,具體重構後的代碼如下所示。

由重構後的代碼容易看出,上面我們提到的臨時常量或者變量都不存在了,取而代之的是兩個查詢方法,對應的查詢方法傳回的就是之前消除的臨時變量或常量的值。

代碼重構(一):函數重構規則

四、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正好相反,在此就不做過多的贅述了。

五、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重構,去掉臨時變量,再次重構後的結果如下所示。

代碼重構(一):函數重構規則

六、split temporary variable-----分解臨時變量:一心不可二用

什麼叫分解臨時變量的,具體說來就是在一個函數中一個臨時變量不能做兩種事情,也就是一個臨時變量不能賦上不同意義的值。如果你這麼做了,那麼對不起,請對該重複使用的臨時變量進行分解,也就是說你需要建立一個新的臨時變量來接收第二次配置設定給第一個臨時變量的值,并為第二個臨時變量命一個确切的名字。

下方第一個函數是重構前的,可以看出temp被重複的指派了兩次的值,如果這兩個值關系不大,而且temp不足以對兩個值的意思進行說明。那麼就說明該段代碼就應該被重構了。當然,重構的做法也是非常簡單的,隻需要術業有專攻即可,各司其職,并且為每個臨時變量命一個合适的名字即可。具體做法如下所示。

 

代碼重構(一):函數重構規則

 七、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 規則進行消除的。所為我們可以再次對上述方法進行重構,重構後的具體代碼如下:

代碼重構(一):函數重構規則

今天的部落客要講了如何對既有代碼中的函數進行重構,在本篇部落格中提到了8大規則。這8大規則在函數代碼重構時時非常實用的,并且也是非常重要的。還是那句話,雖然代碼是使用swift語言實作的,但是代碼重構的手法和思想和語言無關。接下來還會繼續更新關于代碼重構的部落格,敬請期待吧。