重構的兩大目標#
重構追求的核心目標只有兩個:可讀性(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(組合優於繼承)。
以 Bird 和 Penguin 為例:如果 Bird 類別定義了 fly() 方法,那麼繼承 Bird 的 Penguin 就被迫擁有一個不該存在的飛行能力。透過組合(composition),可以將「飛行」抽離為獨立的元件,只在需要時組裝,避免繼承帶來的不合理耦合。
繼承建立的是「is-a」關係,一旦設計有誤就難以修改。組合建立的是「has-a」關係,更靈活也更安全。
透過添加而非修改來實現變更#
Change by addition(透過添加來變更)呼應了 SOLID 原則中的開放封閉原則(Open-Closed Principle):軟體實體應對擴充開放、對修改封閉。
當需要新功能時,優先新增程式碼,而非修改既有程式碼。這能最大程度保護現有行為的穩定性。
重構帶來的好處#
持續重構能在以下三方面帶來實質回報:
| 好處 | 說明 |
|---|---|
| 開發速度 | 乾淨的程式碼讓新功能開發更快 |
| 靈活性 | 良好的結構讓系統更容易適應需求變化 |
| 穩定性 | 局部化的不變量降低了意外 bug 的機率 |
日常重構實踐#
技術債與 Boy Scout Rule#
技術債(technical debt)不會自動消失,只會隨時間累積利息。Boy Scout Rule 提供了一個簡單的應對策略:
離開時讓營地比你到達時更乾淨。
每次接觸程式碼時,順手做一點小改善。日積月累,程式碼品質就能穩步提升。
重構也是學習#
重構不只是改善程式碼,也是理解程式碼的有效方式。透過重新組織一段不熟悉的程式碼,你會被迫深入理解它的邏輯與設計意圖。
Domain 的定義#
Domain(領域)指的是軟體所要解決的業務問題範圍。理解 domain 是寫出好程式碼的前提——你必須先知道「要解決什麼問題」,才能判斷程式碼的結構是否合理。