重構的兩大目標#

重構追求的核心目標只有兩個:可讀性(readability)與可維護性(maintainability)。所有重構技巧最終都服務於這兩件事。

可讀性:溝通程式碼的意圖#

可讀的程式碼能讓讀者快速理解「這段程式碼想做什麼」。提升可讀性的手段包括:

手段說明
命名變數、函式、類別的名稱應直接傳達意圖
慣例遵循團隊或語言的既定風格
空白與排版適當的空行與縮排能引導視覺動線

程式碼被閱讀的次數遠多於被撰寫的次數。花時間在命名和排版上,長期來看是值得的投資。

可維護性:局部化不變量#

可維護性的關鍵在於縮小每次修改需要調查的範圍。當一處變更可能波及整個系統時,系統就變得 fragile(脆弱)。

重構的策略是 localizing invariants(局部化不變量)——將相關的假設與邏輯聚集在同一處,避免散落各地。

什麼是不變量#

Invariant(不變量)是程式碼中隱含的假設——沒有被顯式檢查,卻預期永遠成立的條件。

以雜貨店系統為例:假設有一個全域變數 daysUntilExpiry 代表商品到期天數。多處程式碼可能讀取或修改這個值,但沒有人明確檢查「這個值是否合理」。一旦某處不小心設了負數,其他依賴它的程式碼就可能出錯——這就是 global state(全域狀態)下不變量被破壞的典型場景。

全域狀態是不變量的天敵。越多程式碼能存取同一個狀態,不變量就越容易在不知不覺中被違反。

「不改變行為」的定義#

重構強調「不改變行為」,但這裡的行為是從黑箱測試(black-box testing)的角度定義的:對於相同的輸入,輸出不變。

效能變化是例外——重構可能讓程式碼稍快或稍慢,這通常不被視為行為改變。

重構範圍的拿捏#

重構的 scope(範圍)決定了你能改動多少東西:

  • 範圍越大——可以做更全面的改善,但與其他人的修改產生衝突的風險也越大
  • 範圍越小——衝突風險低,但能改善的幅度有限

實務上需要在兩者之間取得平衡。

重構的三大支柱#

歸納起來,每次重構都應遵守三個原則:

#原則說明
1溝通意圖讓程式碼表達「為什麼這樣寫」
2局部化不變量將假設與限制集中管理
3不影響範圍外的程式碼重構的改動不應產生副作用

組合優於繼承#

Gang of Four 在《Design Patterns》中提出的經典建議:favor composition over inheritance(組合優於繼承)。

BirdPenguin 為例:如果 Bird 類別定義了 fly() 方法,那麼繼承 BirdPenguin 就被迫擁有一個不該存在的飛行能力。透過組合(composition),可以將「飛行」抽離為獨立的元件,只在需要時組裝,避免繼承帶來的不合理耦合。

繼承建立的是「is-a」關係,一旦設計有誤就難以修改。組合建立的是「has-a」關係,更靈活也更安全。

透過添加而非修改來實現變更#

Change by addition(透過添加來變更)呼應了 SOLID 原則中的開放封閉原則(Open-Closed Principle):軟體實體應對擴充開放、對修改封閉。

當需要新功能時,優先新增程式碼,而非修改既有程式碼。這能最大程度保護現有行為的穩定性。

重構帶來的好處#

持續重構能在以下三方面帶來實質回報:

好處說明
開發速度乾淨的程式碼讓新功能開發更快
靈活性良好的結構讓系統更容易適應需求變化
穩定性局部化的不變量降低了意外 bug 的機率

日常重構實踐#

技術債與 Boy Scout Rule#

技術債(technical debt)不會自動消失,只會隨時間累積利息。Boy Scout Rule 提供了一個簡單的應對策略:

離開時讓營地比你到達時更乾淨。

每次接觸程式碼時,順手做一點小改善。日積月累,程式碼品質就能穩步提升。

重構也是學習#

重構不只是改善程式碼,也是理解程式碼的有效方式。透過重新組織一段不熟悉的程式碼,你會被迫深入理解它的邏輯與設計意圖。

Domain 的定義#

Domain(領域)指的是軟體所要解決的業務問題範圍。理解 domain 是寫出好程式碼的前提——你必須先知道「要解決什麼問題」,才能判斷程式碼的結構是否合理。