1. 程式人生 > >程式碼中的壞味道

程式碼中的壞味道

#### 前言 在日常生活中,當我們買的水果放久了之後會發出一種難聞的氣味(“壞味道”),這個時候我們就應該把它扔掉。同樣,程式碼也有“壞味道”,當然確定什麼是和不是程式碼“壞味道”是主觀的,它會隨語言、開發人員和開發方法的不同而不同。在工作當中,很多時候都是在維護之前的專案和在此基礎上增加一些新功能,為了能讓專案程式碼易於理解和維護,要時刻注意程式碼中的“壞味道”,當發現程式碼如果有壞味道了,要及時去重構它使其變成優秀的整潔的程式碼。本文列舉程式碼中一些常見的“壞味道”和相應的重構方案。 #### 過長方法 (Long Method) 這種“壞味道”表現為`方法程式碼行數過長`,方法行數越長,就越難以理解和維護它。一個比較有用的方案就是當你覺得需要對方法中的內容加註釋的時候,你應該將這個程式碼段作為一個新方法提取出來,哪怕有時候僅僅是一行程式碼也可以這麼做,而且方法的命名要儘量做到見名知意,如果區域性變數和引數干擾到方法的提取,則可以使用引入引數物件來進行提取。一般情況下,方法中條件運算子和迴圈是可以將程式碼移至單獨方法的一個很好的程式碼段,對於條件運算子,可以嘗試分解條件,如果方法出現迴圈,可以嘗試提取方法。 #### 過大的類 (Large Class) 這種“壞味道”表現為`一個定義了很多的變數、方法程式碼行數很長的大類`,剛開始的時候類通常都不“大”,一段時間之後,隨著業務的發展新功能的增加,類通常都會就會變得越來越“大”,通常程式設計師都喜歡在原有的類上新增屬性或者新增新的方法的方式來完成功能的開發,當一個類的程式碼行數過多或者功能職責過多的時候,就意味著我們應該將其拆分了,常用有以下三種不同的拆分方式: 1. 提取新類,當大類的部分行為可以分解為一​​個單獨的元件,則可以使用提取類的方式拆分。 2. 提取子類,當大類的部分行為可以以不同的方式實現或在極少數情況下使用,則可以使用提取子類方式拆分。 3. 提取介面,當有必要列出客戶端可以使用的操作和行為的列表的時候,則可以提取介面的方式拆分。 通過重構大類,可以使開發人員無需記住一個類的大量屬性,在許多情況下,將大類分成多個部分可以避免程式碼和功能的重複。 #### 過長引數列表 (Long Parameter List) 這種“壞味道”表現為`一個方法超過三個以上的引數`,當一個方法合併了幾個演算法之後就會可能出現過多引數的情況,這些引數用來控制方法將要執行哪種演算法以及如何執行的。長引數列表也可能是由於我們將類的物件建立過程拆分產生的,想象這麼一個場景,當我們把用於建立方法所需物件的程式碼片段從方法內部移至用於呼叫方法的程式碼,然後建立的物件作為引數傳入方法,這樣,原始類就不再瞭解物件之間的關係,依賴性降低了。當有多個這種物件需要建立之後,每個物件將需要自己的引數,這意味著引數列表會更長。隨著時間的流逝,我們就會越來越難於理解這種方法的長引數列表的具體含義了,清除這種“壞味道”的方式就是將方法的引數列表封裝成一個物件的屬性。通過重構之後,可以使程式碼的可讀性更高,程式碼更簡短,同時可能還會讓你看到以前未被注意的重複程式碼。 #### 過多註釋 (Too Many Comments) 這種“壞味道”表現為`一種方法充滿解釋性的註釋`,當開發者意識到自己的程式碼不直觀或不明顯時一般都會給程式碼加上相應的註釋。寫程式碼註釋的意圖通常都是好的,是為了可以有更好的可讀性讓後面易於維護,在這種情況下,程式碼註釋就會掩蓋了可以改進的可疑程式碼的“壞味道”,好的方法名或者類名就是最好的註釋。 >The best comment is a good name for a method or class. 當我們遇到沒有註釋就無法理解程式碼片段時,首先應該嘗試以無需註釋的方式更改程式碼結構,解決過多註釋通常有以下幾種方式: 1. 提取變數,當如果要使用註釋來解釋複雜的表示式的時候,則可以使用“提取變數”的方式將表示式拆分為可理解的子表示式。 2. 提取方法,當如果註釋解釋了一段程式碼片段,則可以通過提取方法的方式來將這一部分變成一個單獨的方法,這個時候往往方法的名稱就是註釋的內容。 通過提取變數或者提取方法的方式可以使程式碼變得更加直觀和明顯。 #### Switch 濫用(Switch Abuse) 這種“壞味道”表現為`程式碼中存在一個複雜的 switch 運算子`,通常,`if` 條件語句的程式碼可以分散在程式中的不同位置,當需要新增新條件後,就必須找到所有開關程式碼並進行修改。根據經驗,當看到 `switch` 時,你第一時間應該想到要用多型性去重構程式碼。如果 `switch` 是基於型別判斷的,可以使用“用子類替換”或“用狀態/策略替換”。但是當運算子中沒有太多條件,並且它們都使用不同的引數呼叫相同的方法,那麼多型其實是多餘的。在這種情況下,則可以使用“將引數替換為方法”,然後將該方法分解為多個較小的方法,並相應地更改 `switch` ,程式碼經過重構之後改進其的組織方式。當然如果 `switch` 操作只是執行簡單的判斷時,則沒有必要進行程式碼重構。還有就是,在工廠設計模式(工廠方法和抽象工廠)使用開關運算子來選擇建立的類時,也沒有必要對其進行重構。 #### 異曲同工類(Alternative Classes with Different Interfaces) 這種“壞味道”表現為`兩個類有著相同的功能,但方法名稱不同`,產生這種程式碼的原因通常是建立其中一個類的程式設計師可能並不知道功能上等效的類已經存在。清除這種“壞味道”有以下幾種方式: 1. 方法重新命名,重新命名相同功能的方法,使它們在所有替代類中相同。 2. 移動方法、新增引數和泛型方法使得方法的簽名和實現相同。 3. 如果僅僅是重複了方法的部分功能,可以使用提取相同父類的方式重構,在這種情況下,現有的類將成為該父類的子類。 通過重構異曲同工類後,可以去除掉不必要的重複程式碼,從而減少程式碼的行數,同時代碼也會有更好的可讀更易於理解。 #### 臨時變數濫用(Temporary Field) 這種“壞味道”表現為`一些臨時變數僅在某些情況下才獲得其值,在這些情況之外,它們都為空`,通常,當我們在建立一個演算法後需要定義一些臨時變數以供該演算法輸入使用。此時,程式設計師往往會決定在類中為此演算法去建立變數,而不是在方法中建立大量引數,導致這些變數僅在演算法當中才會使用,其它地方都不會使用這些變數。一個應對的方式就是將這些臨時變數和對其進行操作的所有程式碼都提取出來放到單獨的類中。 #### 重複程式碼(Duplicate Code) 這種“壞味道”表現為`兩個或者多個程式碼片段看起來幾乎相同`,當我們多個人同時在同一專案中的不同部分上工作時,通常就會發生複製,產生重複的程式碼。因為正在實現不同的功能,因此可能並不知道其他人已經編寫了類似的程式碼,這些程式碼其實是可以根據自己的需要進行復用的。當代碼中的一些特定部分看起來不同但實際上實現相同的功能時,這樣的程式碼有著更多細微的重複,這種情況下的程式碼重複可能很難找到和修復。如果重複程式碼在`兩個處於相同層次結構的子類`出現時,我們可以通過以下方式進行重構: 1. 提取方法,將重複的程式碼片段提取為方法然後放到共同的父類當中。 2. 如果重複的程式碼在構造方法內部,則將其提取到父類的構造方法當中去,然後再在當前類的構造方法中使用 `super` 的方式呼叫父類構造方法。 3. 如果重複的程式碼結構上相似但又不完全相同,那麼則使用模板方法方式重構。 如果重複程式碼在`兩個或者多個不同的類`出現時,我們可以通過以下方式進行重構: 1. 如果這些類不是層次結構的一部分,可以使用提取共同父類的方式來為這些類建立一個保留所有之前的功能的單個父類。 2. 當很難或者不可能建立父類,那麼可以在其中的任意一個類中使用提取類的方式來重構,然後在其它類中使用剛剛創建出來的類。 通過重構合併重複的程式碼可以簡化程式碼的結構並使其更加簡短和易於後期維護。 #### 總結 本文總結了一些程式碼中常見的“壞味道”並給出了一些解決方法,重構是需要我們開發人員時刻都要去做的,要將重構始終貫穿在整個開發過程中,不斷去發現程式碼中的“壞味道”,不斷的持續的漸進重構。最後不管我們是如何去重構程式碼的,其背後的指導思想都是 [Solid 原則](https://en.wikipedia.org/wiki/S