程式碼結構反映真實世界#

軟體是對真實世界某個面向的模型。真實世界的結構——無論來自領域知識組織流程還是使用者行為——都會滲透進程式碼中。作者將結構空間劃分為四個象限:

象限說明
巨觀架構(Macro-architecture)跨團隊的結構,決定外部 API 與資料所有權
微觀架構(Micro-architecture)團隊內部的結構,包括資料組織與程式碼風格——本書的重構模式屬於此類
組織流程Scrum、Kanban 等工作流程與組織層級
領域專家行為領域中重複出現的行為模式,定義了軟體應有的運作方式

結構傾向於在水平維度上互相映射。組織結構會約束外部 API 的樣貌(Conway’s Law),領域專家的行為模式也會滲入程式碼。如果在程式碼中發現低效率,往往能在專家的工作方式或流程中找到根源。

使用者行為同樣會約束程式碼結構。如果無法重新訓練使用者,他們就是外部約束;如果可以,就能納入重構範圍。但改變人的行為通常比改程式碼更慢、更困難,因此建議先如實建模使用者行為,再逐步提供更高效的功能與教育訓練。

行為嵌入程式碼的三種方式#

不論行為來自何處,嵌入程式碼的方式只有三種:

控制流(Control Flow)#

透過 ifforwhile、遞迴呼叫或單純的程式行來表達行為。這是最常見的做法——大多數人實作 FizzBuzz 時,自然就是用控制流中的 if-else 來處理。

特性:容易做大幅度修改,因為只需搬動陳述式即可改變流程。本書大量的重構模式(如 Extract method、Combine ifs)都在這個層次操作。

資料結構(Data Structure)#

將行為凍結在資料結構中。最經典的例子是 binary searchbinary 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 來暴露隱藏的結構。

共同詞綴 → 封裝#

方法或類別名稱帶有共同前綴或後綴(如 StringProtocolJSONProtocolProtobufProtocol),違反 Never have common affixes 規則。解法是將它們封裝進 namespace 或 package,去除冗餘後綴。

Runtime 型別檢查 → Dynamic dispatch#

使用 typeofinstanceof 或型別轉換來檢查執行期型別,是未利用結構的明確信號。物件導向的 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 型別檢查——都是未利用結構的常見線索