所有成功的軟體都會被修改。神話中,程式碼在初始版本交付後才會大幅修改; 現實中,程式碼在初始開發期間就會經歷大量演化。現代開發實踐更是以程式碼為中心,使演化程度比以往更高。

24.1 軟體演化的類型#

軟體演化(Software Evolution)如同生物演化——有些變異是有益的,有些則不然。

區分軟體演化類型的關鍵在於:修改之後程式品質是提升還是下降?

  • 若用邏輯膠帶和迷信來修復錯誤,品質會劣化
  • 若將修改視為收緊原始設計的機會,品質會提升

軟體演化的基本法則(The Cardinal Rule of Software Evolution):演化應當改善程式的內部品質。

建構期 vs. 維護期#

建構期間的修改通常由原始開發者進行,系統尚未上線,犯錯的代價較低,風格可以更自由。 維護期間則面對生產環境的壓力,修改必須更加謹慎。

演化的哲學#

演化既是風險也是趨近完美的機會。當你必須修改時,應努力讓程式碼變得更好,使未來的修改更容易。 你在開始寫程式時所知的,永遠比寫完之後少——善用每一次修改的機會。

24.2 重構簡介#

重構(Refactoring) 是 Martin Fowler 所定義的:「在不改變程式可觀察行為的前提下,對軟體內部結構所做的修改,使其更容易理解、更易於修改。」

重構的理由(Code Smells)#

以下是常見的「壞味道」(Smells),暗示程式碼需要重構:

壞味道說明
重複的程式碼違反 DRY 原則(Don’t Repeat Yourself)
過長的子程式在物件導向中,超過一個螢幕的子程式極少是必要的
迴圈過長或巢狀過深迴圈內部適合抽取為獨立子程式
類別內聚力差一個類別負責不相關的職責,應拆分
類別介面的抽象層次不一致介面隨時間變形,成為拼湊的怪物
參數列表過長暗示子程式介面的抽象未經深思熟慮
類別內的修改各自獨立暗示應拆成多個類別
修改需要同時更動多個類別程式碼組織不良
繼承階層須平行修改每次建立子類別都得在另一個類別也建立子類別
case 敘述須平行修改考慮改用繼承與多型
相關資料未組織成類別反覆操作同一組資料,應封裝為類別
子程式更多使用其他類別的功能考慮搬移到該類別
基本型別被過度使用如用 int 表示金額,應建立 Money 類別
類別幾乎不做事考慮將職責併入其他類別後刪除
傳遞流浪資料(Tramp Data)資料只是為了傳給下一個子程式而存在
中間人物件什麼都不做考慮直接呼叫目標類別
類別之間過度親密應加強封裝(Encapsulation)
子程式命名不佳趁早更名,越晚成本越高
資料成員是 public應隱藏在存取子程式後面
子類別只用到父類別的一小部分考慮用 has-a 取代 is-a
用註解解釋難懂的程式碼「不要註解壞程式碼——重寫它」
使用全域變數重新審視是否有更乾淨的做法
呼叫子程式前後需要設置/拆卸程式碼檢查介面抽象是否正確
程式碼看似「未來可能會用到」專家共識是不要寫推測性程式碼,把目前的程式碼寫清楚就好

24.3 特定的重構手法#

mindmap
  root((重構手法))
    資料層級
      取代魔術數字
      重新命名變數
      基本型別轉換為類別
    敘述層級
      分解布林表達式
      用多型取代 case
      使用 Null 物件
    子程式層級
      抽取子程式
      分離查詢與修改
      合併相似子程式
    類別實作層級
      上移或下移成員
      抽取為子類別
      合併到父類別
    類別介面層級
      搬移子程式
      隱藏委派
      封裝公開成員變數
    系統層級
      提供工廠方法
      建立權威參考來源
      例外與錯誤碼互換

資料層級重構(Data-Level)#

手法說明
取代魔術數字具名常數取代
重新命名變數用更清楚的名稱
內聯化/引入中間變數將中間變數內聯化(Inline),或反過來引入中間變數以增加可讀性
抽取為子程式將表達式抽取以消除重複
拆分多用途變數將多用途變數拆為多個單一用途變數
基本型別轉換為類別MoneyTemperature
型別代碼轉換為類別或列舉或帶有子類別的繼承結構
陣列轉換為物件-
封裝集合回傳唯讀集合,提供新增/移除方法
用資料類別取代傳統紀錄-

敘述層級重構(Statement-Level)#

手法說明
分解布林表達式引入命名良好的中間變數
移入具名布林函式將複雜布林表達式移入具名函式
合併條件式中重複的片段-
取代迴圈控制變數breakreturn 取代
立即回傳知道答案就回傳,避免巢狀 if-then-else 的層層展開
用多型取代 case 敘述取代重複的 case 敘述
使用 Null 物件取代 null 檢查

子程式層級重構(Routine-Level)#

手法說明
抽取子程式(Extract Method)最常用的重構之一
內聯化子程式將簡單子程式的程式碼內聯化
轉換為類別將過長子程式轉換為類別
取代複雜演算法用簡單演算法取代
新增或移除參數-
分離查詢與修改操作Command-Query Separation
合併相似子程式用參數區分差異
拆分依賴參數的子程式-
傳遞整個物件或特定欄位取決於介面抽象的語意
封裝向下轉型Encapsulate Downcasting

類別實作重構(Class Implementation)#

手法說明
值物件與參考物件之間互轉依物件大小與使用模式決定
用資料初始化取代虛擬子程式子類別僅差在常數值時適用
上移或下移成員在繼承階層中移動子程式、欄位、建構函式
抽取為子類別將特化程式碼抽取為子類別
合併到父類別將相似程式碼合併到父類別

類別介面重構(Class Interface)#

手法說明
搬移子程式將子程式搬移到另一個類別
拆分或消除類別將一個類別拆分為兩個;或將功能不足的類別消除
隱藏委派/移除中間人Hide Delegate 或 Remove Middleman
委派與繼承互換Replace Inheritance with Delegation,或反過來
引入外來子程式或延伸類別Foreign Routine 或 Extension Class
封裝公開的成員變數移除不可變欄位的 Set() 方法
隱藏不對外使用的子程式-
合併父類別與子類別實作非常相似時適用

系統層級重構(System-Level)#

手法說明
建立權威參考來源為無法控制的資料建立(如鏡像 GUI 控制項的資料)
類別關聯方向轉換在單向與雙向之間轉換
提供工廠方法Factory Method 取代簡單建構函式
例外與錯誤碼互換依錯誤處理策略保持一致

24.4 安全的重構#

重構是強大的工具,但若誤用會造成比好處更多的傷害。

安全重構的守則#

#守則說明
1備份起始程式碼存入版本控制或備份目錄
2保持重構規模小確保完全理解每次變更的影響
3一次只做一個重構重構後重新編譯與測試,再進行下一個
4列出步驟清單從 A 點到 B 點的重構計畫
5設立暫存區(Parking Lot)記錄重構途中發現但不急於處理的想法
6頻繁建立檢查點避免走入死胡同時無法回退
7善用編譯器警告設定最嚴格的警告層級
8重新測試搭配回歸測試,並新增測試案例來驗證新程式碼
9審查變更小修改的錯誤率反而更高

研究顯示,修改 1-5 行程式碼時的出錯機率最高。一個組織在引入單行變更的審查後,錯誤率從 55% 降到 2%。 對待簡單的修改,就像對待複雜的修改一樣謹慎。

不適合重構的時機#

  • 不要用重構掩飾「邊寫邊修」——重構針對的是可運作的程式碼。對壞掉的程式碼東改西改不是重構,那是亂改(Hacking)
  • 避免用重構代替重寫——如果程式碼需要大規模翻修,考慮從頭重新設計與實作

24.5 重構策略#

重構同樣適用 80/20 法則——把時間花在能帶來 80% 效益的 20% 重構上。

時機說明
新增子程式時檢查相關子程式是否組織良好
新增類別時趁機重構關聯密切的既有類別
修正缺陷時利用對程式碼的理解,改善可能有類似問題的其他程式碼
針對高錯誤率模組團隊裡人人害怕碰的程式碼,正是最需要重構的地方
針對高複雜度模組研究顯示,專注於最高複雜度的模組能顯著改善品質
在維護環境中不需要修改的程式碼不必重構,但碰到的程式碼要讓它比原來更好
策略:定義乾淨程式碼與髒亂程式碼之間的介面

對於老舊系統,可將程式碼區分為三個區域:

  1. 混亂的現實世界(Messy Real World)——未經整理的遺留程式碼
  2. 介面層(Interface)——隔離乾淨程式碼與髒亂程式碼
  3. 整潔的理想世界(Nice Tidy Ideal World)——經過重構的高品質程式碼

每次碰到髒亂程式碼時,就將它提升至目前的編碼標準,逐步將程式碼搬移到「理想世界」那一側。

更多資源#

  • Fowler, Martin. Refactoring: Improving the Design of Existing Code (1999)——重構的權威指南,包含本章摘要的各項重構手法的詳細討論與逐步程式碼範例
  • 重構的過程與修正缺陷有許多共通之處,可參閱 Section 23.3
  • 重構的風險管理與程式碼調校類似,可參閱 Section 25.6

要點#

  • 程式碼變更在初始開發與發布後都是必然的事實
  • 軟體在修改過程中可能改善也可能劣化;軟體演化的基本法則是內部品質應隨演化而提升
  • 成功重構的關鍵之一是學會辨識眾多警示訊號(Smells)
  • 另一個關鍵是熟悉大量具體的重構手法
  • 最後一個關鍵是擁有安全重構的策略——某些做法比其他做法更好
  • 開發期間的重構是改善程式的最佳時機,善用這些機會