1. 程式人生 > >【重構】 程式碼的壞味道總結 Bad Smell (一) (重複程式碼 | 過長函式 | 過大的類 | 過長引數列 | 發散式變化 | 霰彈式修改)

【重構】 程式碼的壞味道總結 Bad Smell (一) (重複程式碼 | 過長函式 | 過大的類 | 過長引數列 | 發散式變化 | 霰彈式修改)

膜拜下 Martin Fowler 大神 , 開始學習 聖經 重構-改善既有程式碼設計 .

程式碼的壞味道就意味著需要重構, 對程式碼的壞味道瞭然於心是重構的比要前提;

.

作者 : 萬境絕塵

.

1. 重複程式碼 (Duplicated Code)

用到的重構方法簡介 : Extract Method(提煉函式), Pull Up Method(函式上移), From Template Method(塑造模板函式), Substitute Algorithm(替換演算法), Extract Class(提煉類);

-- Extract Method(提煉函式) : 將重複的程式碼放到一個函式中, 並讓函式名稱清晰的解釋函式的用途;

-- Pull Up Method(函式上移) : 將函式從子類移動到父類中;

-- From Template Method(塑造模板函式) : 不同子類中某些函式執行相似操作, 細節上不同, 可以將這些操作放入獨立函式中, 這些函式名相同, 將函式上移父類中.

-- Substitute Algorithm(替換演算法) : 將函式的本體替換成另外一個演算法;

-- Extract Class(提煉類) : 建立一個新類, 將相關的函式 和 欄位 從舊類搬移到新類;

重複程式碼壞處 : 重複的程式碼結構使程式變得冗長, 這個肯定要優化, 不解釋;

同類函式重複程式碼 : 同一個類中 兩個函式 使用了相同的表示式;

-- 解決方案 : 使用 Extract Method(提煉函式) 方法提煉出重複的程式碼, 兩個函式同時呼叫這個方法, 代替使用相同的表示式;

兄弟子類重複程式碼 : 一個父類有兩個子類, 這兩個子類存在相同的表示式;

-- 程式碼相同解決方案 : 對兩個子類 使用 Extract Method(提煉函式)方法, 然後將提煉出來的程式碼 使用 Pull Up Method(函式上移)方法, 將這段程式碼定義到父類中去;

-- 程式碼相似解決方案 : 使用 Extract Method(提煉函式)方法 將相似的部分 與 差異部分 分割開來, 將相似的部分單獨放在一個函式中;

-- 進一步操作 : 進行完上面的操作之後, 可以運用 From Template Method(塑造模板函式)

獲得一個 Template Method 設計模式, 使用模板函式將相似的部分設定到模板中, 不同的部分用於模板的引數等變數;

-- 演算法切換 : 如果模板中函式的演算法有差異, 可以選擇比較清晰的一個, 使用Substitute Algorithm(替換演算法) 將不清晰的演算法替換掉;

不相干類出現重複程式碼 : 使用Extract Class(提煉類) 方法, 將重複的程式碼提煉到一個重複類中去, 然後在兩個類中 使用這個提煉後的新類; 

-- 提煉類存在方式 : 將提煉後的程式碼放到兩個類中的一個, 另一個呼叫這個類, 如果放到第三個類, 兩個類需要同時引用這個類;

2. 過長函式(Long Method)

用到的重構方法 : Extract Method(提煉函式)Replace Temp with Query(以查詢取代臨時變數)Introduce Parameter Object(引入引數物件)Preserve Whole Object(保持物件完整), Decompose Conditional(分解條件表示式);

-- Extract Method(提煉函式) : 將程式碼放到一個新函式中, 函式名清晰的說明函式的作用;

-- Replace Temp with Query(以查詢取代臨時變數) : 程式中將表示式結果放到臨時變數中, 可以將這個表示式提煉到一個獨立函式中, 呼叫這個新函式 去替換 這個臨時變量表達式, 這個新函式就可以被其它函式呼叫;

-- Introduce Parameter Object(引入引數物件) : 將引數封裝到一個物件中, 以一個物件取代這些引數;

-- Preserve Whole Object(保持物件完整) : 從某個物件中取出若干值, 將其作為某次函式呼叫時的引數, 由原來的傳遞引數 改為 傳遞整個物件, 類似於 Hibernate;

-- Replace Method with Method Object(以函式物件取代函式) : 大型函式中有很多 引數 和 臨時變數, 將函式放到一個單獨物件中, 區域性變數 和 引數 就變成了物件內的欄位, 然後可以在 同一個物件中 將這個 大型函式 分解為許多 小函式;

-- Decompose Conditional(分解條件表示式) : 將 if then else while 等語句的條件表示式提煉出來, 放到獨立的函式中去; 

小函式優點 : 小函式具有更強的 解釋能力, 共享能力, 選擇能力, 小函式維護性比較好, 擁有小函式的類活的比較長;

-- 長函式缺點 : 程式越長越難理解;

-- 函式開銷 : 早期程式語言中子程式需要額外的開銷, 所以都不願意定義小函式. 現在面嚮物件語言中, 函式的開銷基本沒有;

-- 函式名稱 : 小函式多, 看程式碼的時候經常轉換上下文檢視, 這裡我們就需要為函式起一個容易懂的好名稱, 一看函式名就能明白函式的作用, 不同在跳轉過去理解函式的含義;

分解函式結果 : 儘可能分解函, 即使函式中只有一行程式碼, 哪怕函式呼叫比函式還要長, 只要函式名解釋程式碼用途就可以;

-- 分解時機 : 當我們需要添加註釋的時候, 就應該將要註釋的程式碼寫入到一個獨立的函式中, 並以程式碼的用途命名;

-- 關鍵 : 函式長度不是關鍵, 關鍵在於 函式 是 "做什麼", 和 "如何做";

常用分解方法 : Extract Method(提煉函式) 適用於 99% 的過長函式情況, 只要將函式中冗長的部分提取出來, 放到另外一個函式中即可;

引數過多情況 : 如果函式內有大量的 引數 和 臨時變數, 就會對函式提煉形成阻礙, 這時候使用 Extract Method(提煉函式) 方法就會將許多引數 和 臨時變數當做引數傳入到 提煉出來的函式中;

-- 消除臨時變數 : 使用 Replace Temp with Query(以查詢取代臨時變數) 方法消除臨時元素;

-- 消除過長引數 : 使用 Introduce Parameter Object(引入引數物件) Preserve Whole Object(保持物件完整) 方法 可以將過長的引數列變得簡潔一些;

-- 殺手鐗 : 如果使用了上面 消除臨時變數和過長引數的方法之後, 還存在很多 引數 和 臨時變數, 此時就可以使用 Replace Method with Method Object(以函式物件取代函式方法) ;

提煉程式碼技巧

-- 尋找註釋 : 註釋能很好的指出 程式碼用途 和 實現手法 之間的語義距離, 程式碼前面有註釋, 就說明這段程式碼可以替換成一個函式, 在註釋的基礎上為函式命名, 即使註釋下面只有一行程式碼, 也要將其提煉到函式中;

-- 條件表示式 : 當 if else 語句, 或者 while 語句的條件表示式過長的時候, 可以使用Decompose Conditional(分解條件表示式) 方法, 處理條件表示式;

-- 迴圈程式碼提煉 : 當遇到迴圈的時候, 應該將迴圈的程式碼提煉到一個函式中去;

3. 過大的類 (Large Class)

用到的重構方法 : Extract Class(提煉類), Extract Subclass(提煉子類), Extract Interface(提煉介面), Duplicate Observed Data(複製被監視的資料);

-- Extract Class(提煉類) : 一個類中做了兩個類做的事, 建立一個新類, 將相關的欄位和函式從舊類中搬移到新類;

-- Extract Subclass(提煉子類) : 一個類中的某些特性只能被一部分例項使用到, 可以新建一個子類, 將只能由一部分例項用到的特性轉移到子類中;

-- Extract Interface(提煉介面) : 多個客戶端使用類中的同一組程式碼, 或者兩個類的介面有相同的部分, 此時可以將相同的子集提煉到一個獨立介面中;

-- Duplicate Observed Data(複製被監視的資料) : 一些領域資料放在GUI控制元件中, 領域函式需要訪問這些資料; 將這些資料複製到一個領域物件中, 建立一個觀察者模式, 用來同步領域物件 和 GUI物件的重要資料;

例項變數太多解決方案 : 使用 Extract Class (提煉類) 方法將一些變數提煉出來, 放入新類中;

-- 產生原因 : 如果一個類的職能太多, 在單個類中做太多的事情, 這個類中會出現大量的例項變數; 

-- 例項變數多的缺陷 : 往往 Duplicate Code(重複程式碼) 與 Large Class(過大的類)一起產生的;

-- 選擇相關變數 : 選擇類中相關的變數提煉到一個新類中, 一般字首, 字尾相同的變數相關性較強, 可以將這些相關性較強的變數提煉到一個類中;

-- 子類提煉 : 如果一些變數適合作為子類, 使用Extract Subclass(提煉子類) 方法, 可以建立一個子類, 繼承該類, 將提煉出來的相關變數放到子類中;

-- 多次提煉 : 一個類中定義了20個例項變數, 在同一個時刻, 只使用一部分例項變數, 比如在一個時刻只使用5個, 在另一時刻只使用4個 ... 我們可以將這些例項變數多次使用 提煉類 和 子類提煉方法;

程式碼太多解決方案

-- 程式碼多的缺陷 : 太多的程式碼是 程式碼重複, 混亂, 最終走向專案死亡的源頭;

-- 簡單解決方案 : 使用 Extract Method (提煉函式) 方法, 將重複程式碼提煉出來;

-- 提煉類程式碼技巧 : 使用 Extract Class(提煉類)Extract Subclass(子類提煉) 方法對類的程式碼進行提煉, 先確定客戶端如何使用這個類, 之後運用 Extract Interface(提煉介面) 為每種使用方式提煉出一個介面, 可以更清楚的分解這個類;

-- GUI類提煉技巧 : 使用 Duplicate Observed Data(複製被監視的資料) 方法, 將資料 和 行為 提煉到一個獨立的物件中, 兩邊各保留一些重複資料, 用來保持同步; 

4. 過長引數列 (Long Parameter List)

使用到的重構方法簡介 : Replace Parameter with Method(以函式取代引數), Preserve Whole Object(保持物件完整), Introduce Parameter Object(引入引數物件);

-- Replace Parameter with Method(以函式取代引數) : 物件呼叫 函式1, 將結果作為 函式2 的引數, 函式2 內部就可以呼叫 函式1, 不用再傳遞引數了; 

-- Preserve Whole Object(保持物件完整) : 將物件中的一些欄位是函式的引數, 直接將物件作為函式的引數, 由傳遞多個引數改為傳遞封裝好的物件;

-- Introduce Parameter Object(引入引數物件) : 將函式引數封裝在一個物件中;

引數列過長

-- 函式資料來源 : ① 引數, 將函式中所需的資料都由引數傳入; ② 將函式中所用到的資料設定在全局資料中, 儘量不要使用全域性資料;

-- 物件引數 : 使用物件封裝引數, 不需要把函式需要的所有資料用引數傳入, 只需要將函式用到的資料封裝到物件中即可;

-- 面向物件函式引數少 : 面向物件程式的函式, 函式所用的資料通常在類的全域性變數中, 要比面向過程函式引數要少;

普通引數和物件引數對比

-- 引數過長缺陷 : 太多的引數會造成函式 呼叫之間的 前後不一致, 不易使用, 一旦需要更多資料, 就要修改函式引數結構;

-- 物件引數優點 : 使用物件傳遞函式, 如果需要更多的引數, 只需要在物件中新增欄位即可;

引數的其它操作

-- 函式取代引數 : 在物件中 執行一個 函式1 就可以取代 函式2 的引數, 就要使用 Replace Parameter with Method(以函式取代引數) 方法;

-- 物件代替引數 :  函式中來自 同一個物件的 多個引數 可以封裝在這個物件中, 可以將這個封裝好的物件當做引數, 使用Preserve Whole Object(保持物件完整) 方法;

-- 建立引數物件 : 如果找不到合適的物件封裝這些引數資料, 可以使用 Introduce Parameter Object(引入引數物件) 方法制造一個引數物件;

物件依賴與函式引數之間的平衡 : 二者是相對的, 下面一定要選擇一種不利狀況;

-- 避免依賴 : 函式引數傳遞物件, 那個函式所在的物件 與 這個引數物件依賴關係很緊密, 耦合性很高, 這時候就要避免依賴關係, 將資料從物件中拆出來作為引數;

-- 引數太長 : 如果引數太長, 或者變化太頻繁, 就要考慮是否選擇依賴;

5. 發散式變化 (Divergent Change)

對於這個在我所在的研發團隊中這個問題很嚴重, 因為做的是遠端醫療系統, 在Android上要支援許多醫療裝置, 每次新增醫療裝置都會死去活來;

使用到的重構方法簡介 : Extract Class(提煉類);

期望效果 : 當我們新增新功能的時候, 只需要修改一個地方即可, 針對外界變化相應的修改, 只發生在單一類中, 如果做不到這一點, 就意味著程式有了壞味道 Divergent Change;

發散式變化

-- 出現效果 : 如果對程式進行例行維護的時候, 新增修改元件的時候, 要同時修改一個類中的多個方法, 那麼這就是 Divergent Change;

-- 修改方法 : 找出造成發散變化的原因, 使用 Extract Class(提煉類) 將需要修改的方法集中到一個類中;

6. 霰彈式修改 (Shotgun Surgery)

使用到的重構方法簡介 : Move Method(搬移函式), Move Field(搬移欄位), Inline Class(內聯化類);

-- Move Method(搬移函式) : 類A 中的 方法A 與 類B 交流頻繁, 在類B中建立一個與 方法A 相似的 方法B, 從方法A 中 呼叫 方法B, 或者直接將方法A刪除;

-- Move Field(搬移欄位) : 類A 中的 欄位A 經常被 類B 用到, 在類B 中新建一個欄位B, 在類B 中儘量使用欄位B;

-- Inline Class(內聯化類) : 類A 沒有太多功能, 將類A 的所有特性搬移到 類B中, 刪除類A ;

霰彈式修改壞味道 : 遇到的每種變化都需要在許多不同類內做出小修改, 即要修改的程式碼散佈於四處, 不但很難找到, 而且容易忘記重要的修改, 這種情況就是霰彈式修改;

-- 注意霰彈式修改 與 發散式變化 區別 : 發散式變化是在一個類受多種變化影響, 每種變化修改的方法不同, 霰彈式修改是 一種變化引發修改多個類中的程式碼;

-- 目標 : 使外界變化需要修改的類 趨於一一對應;

重構霰彈式修改

-- 程式碼集中到某個類中 : 使用 Move Method(搬移函式)Move Field(搬移欄位) 把所有需要修改的程式碼放進同一個類中;

-- 程式碼集中到新建立類中 : 沒有合適類存放程式碼, 建立一個類, 使用 Inline Class(內聯化類) 方法將一系列的行為放在同一個類中;

-- 造成分散式變化 : 上面的兩種操作會造成 Divergent Change, 使用Extract Class 處理分散式變化;

.

作者 : 萬境絕塵

.