【重构】 代码的坏味道总结 Bad Smell (一) (重复代码 | 过长函数 | 过大的类 | 过长参数列 | 发散式变化 | 霰弹式修改)
膜拜下 Martin Fowler 大神 , 開始學習 圣經 重構-改善既有代碼設計 .
代碼的壞味道就意味著需要重構, 對代碼的壞味道了然于心是重構的比要前提;
.
作者 : 萬境絕塵
轉載請注明出處 :?http://blog.csdn.net/shulianghan/article/details/20009689
.
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 處理分散式變化;
.
作者?:?萬境絕塵
轉載請注明出處?:?http://blog.csdn.net/shulianghan/article/details/20009689
.
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀
總結
以上是生活随笔為你收集整理的【重构】 代码的坏味道总结 Bad Smell (一) (重复代码 | 过长函数 | 过大的类 | 过长参数列 | 发散式变化 | 霰弹式修改)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【Android 应用开发】Androi
- 下一篇: 【面向对象设计模式】 适配器模式 (二