程式碼結構反映真實世界#
軟體是對真實世界某個面向的模型。真實世界的結構——無論來自領域知識、組織流程還是使用者行為——都會滲透進程式碼中。作者將結構空間劃分為四個象限:
| 象限 | 說明 |
|---|---|
| 巨觀架構(Macro-architecture) | 跨團隊的結構,決定外部 API 與資料所有權 |
| 微觀架構(Micro-architecture) | 團隊內部的結構,包括資料組織與程式碼風格——本書的重構模式屬於此類 |
| 組織流程 | Scrum、Kanban 等工作流程與組織層級 |
| 領域專家行為 | 領域中重複出現的行為模式,定義了軟體應有的運作方式 |
結構傾向於在水平維度上互相映射。組織結構會約束外部 API 的樣貌(Conway’s Law),領域專家的行為模式也會滲入程式碼。如果在程式碼中發現低效率,往往能在專家的工作方式或流程中找到根源。
使用者行為同樣會約束程式碼結構。如果無法重新訓練使用者,他們就是外部約束;如果可以,就能納入重構範圍。但改變人的行為通常比改程式碼更慢、更困難,因此建議先如實建模使用者行為,再逐步提供更高效的功能與教育訓練。
行為嵌入程式碼的三種方式#
不論行為來自何處,嵌入程式碼的方式只有三種:
控制流(Control Flow)#
透過 if、for、while、遞迴呼叫或單純的程式行來表達行為。這是最常見的做法——大多數人實作 FizzBuzz 時,自然就是用控制流中的 if-else 來處理。
特性:容易做大幅度修改,因為只需搬動陳述式即可改變流程。本書大量的重構模式(如 Extract method、Combine ifs)都在這個層次操作。
資料結構(Data Structure)#
將行為凍結在資料結構中。最經典的例子是 binary search 與 binary search tree(BST) 的關係:binary search 是一個演算法,透過反覆對半搜尋來找到目標元素;BST 則是一棵樹狀結構,其不變量(invariant) 保證左子樹的值都小於父節點、右子樹的值都大於父節點。binary search 的行為被編碼進了 BST 的結構中。

Figure 11.1: Binary search and BST
特性:比起控制流,小幅修改更安全(因為有更好的型別安全與局部性),但不容易做大幅度改動,除非變動點恰好對齊已有的結構。重構模式 Replace type code with classes 和 Introduce strategy pattern 都是把控制流中的結構搬進資料結構。
資料本身(Data)#
將行為編碼在資料中——例如用陣列中的參考(reference)來間接呼叫自身。這種方式最難安全地使用,因為編譯器無法提供任何支援,容易落入停機問題(halting problem)的盲區。在業界最常見的形式是重複資料(duplicated data),可能帶來一致性問題。
由於資料中的結構極難安全維護,作者建議盡量將其轉換為控制流或資料結構的形式。
重構就是在三者之間搬移#
重構的本質是在不改變行為的前提下,管理同一種方式內的重複,或是將結構從一種方式搬到另一種方式。
flowchart LR
CF["控制流\nif / for / while"] <-->|"Replace type code\nwith classes\nIntroduce strategy"| DS["資料結構\ninterface + classes"]
DS <-->|"應盡量避免"| D["資料本身\n參考 / 重複資料"]
CF <-->|"應盡量避免"| D
style CF fill:#e1f5fe
style DS fill:#e8f5e9
style D fill:#fff3e0何時重構、何時保守#
重構會固化現有結構,讓程式碼更容易接受類似的變動,並在預期的變化方向上放置變異點(variation points)。但在不確定性高的子系統中,過早加入變異點反而會增加複雜度、隱藏其他結構。
| 情境 | 不確定性 | 策略 |
|---|---|---|
| 新功能 / 新子系統 | 高 | 先用 enum 和迴圈等容易修改的結構,搭配密集測試 |
| 成熟穩定的程式碼 | 低 | 結構方向明確後,透過重構將結構固化,程式碼的穩固程度應反映我們對其方向的信心 |
如果程式碼不會再變,就沒有重構的必要。如果變化不可預測,只重構到不會造成脆弱性(fragility)的程度即可。如果有明確的變化模式,就針對過去發生過的變化類型做重構。
觀察而非預測:採用經驗方法#
與其試圖預測變化方向(change vector),不如使用經驗方法(empirical techniques)。作者提到 Toyota Kata、Evidence-Based Management、Popcorn Flow 等結構化實驗方法,強調軟體開發應走向更科學的方向。
作者分享了一個關於西洋棋實作的故事:一位開發者主張用 interface 和 classes 來實作棋子,以便「維護」。但西洋棋規則已經 500 年沒變過——這說明即使擁有強大的工具,也不代表每次都該用。應觀察程式碼的實際變化趨勢,而非臆測。
安全重構的四種保障#
在不完全理解程式碼的情況下重構仍然可行,因為結構就在程式碼中,我們可以遵循既有結構使用健全的重構模式。但人會犯錯,因此需要多重保障:
| 保障方式 | 說明 |
|---|---|
| 測試(Testing) | 自動化功能測試是最常見的方式,但測試可能無法涵蓋所有錯誤發生的位置 |
| 精通(Mastery) | 將重構分解成極小的步驟,反覆練習直到成為機械化動作,降低犯錯機率 |
| 工具輔助(Tool Assistance) | 現代 IDE 內建的自動重構功能,消除人為因素 |
| 形式驗證(Formal Verification) | 對於失敗代價極高的系統(飛機、火星探測器),可用定理證明器(proof assistant)機械化驗證正確性 |
| 容錯(Fault Tolerance) | 搭配 feature toggling 的自動回滾機制,即使重構出錯也能自動恢復 |
沒有任何單一方法是萬無一失的,實務上通常混合使用多種保障,並在某個點接受殘餘風險。
識別未被利用的結構#
程式碼中到處潛藏著結構——來自領域、溝通方式、思維偏誤。利用這些結構可以讓程式碼在高變動速率下更穩定,但必須判斷結構是否會持續存在。來自領域的結構通常比軟體本身更古老、更成熟,可以安全地利用;而組織流程與團隊的壽命遠短於軟體,將其烘焙進系統中往往得反覆拆解。
空白行 → Extract method / Encapsulate data#
開發者常用空白行來表達心智分組。這些空白行揭示了作者對問題拆解方式的理解,是低成本、低風險的結構線索。看到陳述式之間的空白行,考慮 Extract method;看到欄位之間的空白行,考慮 Encapsulate data。
重複 → 統一#
重複出現在陳述式、方法、類別中,處理流程一致:陳述式提取為方法,方法封裝為類別。若類別間結構相似但不完全相同,可使用 Introduce strategy pattern 來暴露隱藏的結構。
共同詞綴 → 封裝#
方法或類別名稱帶有共同前綴或後綴(如 StringProtocol、JSONProtocol、ProtobufProtocol),違反 Never have common affixes 規則。解法是將它們封裝進 namespace 或 package,去除冗餘後綴。
Runtime 型別檢查 → Dynamic dispatch#
使用 typeof、instanceof 或型別轉換來檢查執行期型別,是未利用結構的明確信號。物件導向的 dynamic dispatch 透過 interface 提供了更強大的替代方案。如果控制 A 和 B 的原始碼,就引入共同 interface 並使用 Push code into classes 消除 if;如果不控制原始碼,則將型別檢查推到程式碼的邊緣。
mindmap
root((未利用的結構線索))
空白行
Extract method
Encapsulate data
重複
統一陳述式和方法
Introduce strategy pattern
共同詞綴
封裝進 namespace
Never have common affixes
Runtime 型別檢查
Dynamic dispatch
Push code into classes本章重點#
- 程式碼鏡射了開發人員、流程與領域的行為
- 控制流編碼的行為便於大幅修改;資料結構編碼的行為提供型別安全與局部性;資料編碼的行為應盡量避免
- 重構要麼管理同一方式內的重複,要麼在三種方式之間搬移結構
- 不確定時保守重構,確定後才固化結構
- 用經驗方法而非猜測來引導重構方向
- 空白行、重複、共同詞綴、runtime 型別檢查——都是未利用結構的常見線索