最近在看《重構》一書,收獲頗多。
重構,是有跡可循的。某些模式的代碼,向我們昭示着重構的可能,書中作者稱之為“代碼的壞味道”。
一:重復的代碼
在程序中出現兩次以上的程序結構,應該進行重構:
1:在一個函數中出現重復的結構(如:多個if語句),就要考慮優化算法,使用更簡潔、高效的寫法。
2:同一個類中出現兩次以上相同結構的代碼,則提取出來作為一個函數。
3:兩個互為兄弟的子類之間含相同代碼,則先提取出來作為獨立函數,然后上推到父類中。
4:兩個互為兄弟的子類之間含相似代碼,則先把相同部分提取作為函數1,不同部分作為函數2,然后在函數3中調用函數1、函數2,並把相同簽名的函數上推到父類。【這樣,由於子類各自重寫了函數1、2,那么子類的函數3產生的結果就不同。此法名為“塑造模版函數”】
5:兩個毫不相關的類中重復代碼,則把重復代碼提取到一個新的獨立類中,然后在原來的類中通過新類進行調用。(例如:提取到工具類中)
二:過長函數
函數是功能的基本單元,一個函數一個盡量承擔一個職責。如果一個函數中,做了多步工作,則應該進行重構:
1:我們在編寫函數時,如果需要用注釋來說明某一塊代碼時,則應該優先考慮把這部分代碼作為一個函數來定義,並且通過函數名來說明其用途;在重構長函數時,這也是特征之一 —— “函數中哪里需要用注釋說明其用途,則嘗試提取出來作為獨立函數,用函數名表達其用途”
2:對已有長函數進行分解:以單一功能為指標,提取每一部分代碼進獨立函數,最后原函數只需通過一系列調用語句,引用被提取出去的函數即可。
3:長函數中的臨時變量:在原函數中,如有使用一些臨時變量來接收某個函數調用結果的,則把這些臨時變量直接用函數調用語句代替。
4:過長函數參數列:過長函數參數列表是函數調用出錯的主要原因,可以新建一個參數類,把參數作為類成員,而調用時只需傳遞一個參數類對象即可。
5:有太多臨時變量和參數不能替代或提取:使用函數對象法:新建一個新的類,在其中通過一個成員變量,引用原來的類;把原函數中用到的臨時變量、參數,全部作為類成員字段;定義一個函數,通過使用成員字段,實現與原函數一樣的功能;最后,將原函數改造為:新建函數功能類對象(this,原臨時變量,參數 作為構造參數),調用功能函數,並把結果返回。
6:條件表達式改造:用於if語句的判斷表達式往往是造成代碼可讀性下降的原因之一,某些判斷語句用到的數據需要通讀上下文才能理解。可以將條件語句提取出來,作為一個獨立函數,通過函數名表達其判斷內容,而函數內根據判斷語句返回true 或 false 即可。
7:循環語句改造:循環語句塊同樣可以承擔單一職責,因此可以提取出來作為獨立函數,函數名表達其用途。
三:過大的類
類是面向對象而設計的,如果一個類中包含了太多與其本身無關的功能時,就要考慮重構:
1:將與本類無關的變量、函數,提取出來作為一個新類;
2:如果提取出來的變量、函數,適合作為一個子類,則使用提取子類法;
四:過長參數列表
1:如果參數是某個函數的調用結果,則直接使用函數調用語句作為參數;
2:如果某幾個參數是屬於某一個類的字段,則使用該類的對象作為參數,以保持對象的完整性;
3:剩下的雜亂無章、缺乏歸屬對象的參數,則為它們制造一個類,用以容納這些參數,以參數類對象作為函數參數。
五:發散式變化【一個類受多種變化影響】
類的設計要有可擴展性,並且修改要容易進行。如果一個類需要引入不同變化時,對於每種變化,需要修改多個地方,則需要進行重構:
以變化為基本單元,對於某一種變化,所引起的修改,提取到一個新的類中,使得每種變化都分別對應於一個類而進行。
六:霰彈式變化【一個變化,影響多個類】
如果有一種變化,需要在多個不同類中進行修改,則需要進行重構:
根據這個變化所引起的修改,把它們全部提取到一個類中。
七:放錯位置
如果一個類中,有函數對另一個類的內容調用頗多,則需要進行重構:
1:如果是一個函數過多調用另一個類的數據,則把該函數“搬移”到被那個類去;
2:如果是一個函數中部分代碼過多調用另一個類中數據,則先把該部分代碼提煉為獨立函數,然后再搬移。
八:零散數據
如果有一些數據,常常在一起出現,例如:作為函數參數經常出現,則需要進行重構:
把這些零散的數據提煉到一個新的類中,以對象為單位來組織、使用這些數據。
九:替換基本數據類型
對於某些小規模、少字段的信息,雖然可以用幾個基本數據類型來表達出來,但是這些零散數據一旦分開使用,就讓人摸不清用途。所以需要重構:
1:用小對象組合零散數據:例如:帶有數值與貨幣種類的money類、由start和end字段組成的range類等,類名清晰易懂。
2:不影響類行為,只用於表示某內容的類型碼替換:
3:影響類行為的類型碼替換:
十:switch語句塊重構
將switch語句提煉到獨立函數,盡量用多態來取代case判斷語句的基本數據類型,最后把該函數上推到父類中去。
十一:平行繼承
如果有兩個繼承體系,體系一的子類用途與體系二的子類用途相似,則需要重構:
在體系一中引用體系二子類實例,將原子類中的函數改寫為調用體系二子類實例中函數。
十二:冗余類
如果有一些子類、獨立類,沒有承擔明確的用途,那么就需要重構:
1:如果是沒明確職責的子類,則折疊繼承體系——把子類內容合並到父類去,取消子類。
2:如果是沒明確職責的獨立類,則將其內容合並到最頻繁調用它的類中去。
十三:過長的調用鏈
如果存在一個類調用類2,類2調用類3...造成一長串調用關系,則需要重構:
1:隱藏委托關系:把 類1對象.類2對象字段.getXX() 形式的代碼,在類1中進行封裝,定義 get類2XX() 函數,在函數中通過類2對象.getXX()調用,並把結果返回。
十四:去掉中間人
1: 過度委托(一類中超過一半方法需要靠委托類來調用其他類方法):則隱藏中間人,去掉委托類,讓調用者直接與負責的對象打交道。
2:如果只有少數函數需要委托類來調用其他類方法:則把這些函數放進調用端,直接用調用端.XX()調用即可。
3:如果委托類還有其他行為,則使用“繼承取代委托”,繼承實際負責類作為子類,從而擴展原對象的行為,又可以調用原對象的方法。
十五:訪問私有
如果兩個類之間彼此過多訪問private內容,則需要重構:
1:把經常需要互相調用的內容提取到一個新的類中,在新類中光明正大地直接調用;
2:子類可以獨立成類:則用委托取代繼承。將子類作為一個獨立的類來定義,在其中使用一個原父類對象進行內容調用。
十六:異曲同工的內容
如果有功能相同的函數、代碼,則將它們進行統一。
十七:為原有類庫添加函數
如果需要在原有的類庫基礎上添加新函數,可以使用繼承原類庫的功能類,在子類中添加新函數,在程序中使用自定義的子類即可。
十八:類中的字段封裝
類中的字段應該保持私密性:
1:普通字段封裝:將public改為private,並定義public修飾的setter/getter函數。
2:集合字段封裝:對於集合類型的字段,定義public修飾的remove/add函數,在函數中通過集合字段本身調用remove\add操作。
3:只讀字段:對於一些定義了之后就不再修改的字段,我們應該在類的構造函數中進行賦值,然后只提供getter函數,隱藏setter函數。
十九:拒絕繼承
如果一個子類只需要父類中的少數內容,那就應該用委托取代繼承,避免在子類中無謂地實現父類的接口。
二十:注釋過多
注釋可以增強代碼可讀性,但是注釋也為我們指明了重構的方向。
1:需要用注釋來說明一個代碼塊的用途時,嘗試將其提取為獨立函數,用函數名來表達用途;
2:需要注釋來說明某種條件、狀態時,使用斷言。