本章核心#

上一章結尾留下的 updateTile 仍違反多條規則。本章的主題是合併相似的程式碼——無論是相似的 class、相似的條件判斷、還是跨 class 的重複邏輯,都有對應的重構模式可以處理。同時引入條件運算的算術規則 (condition arithmetic)、UML 類別圖基礎,以及最精巧的 INTRODUCE STRATEGY PATTERN


Refactoring Pattern: UNIFY SIMILAR CLASSES#

問題#

StoneFallingStone 幾乎完全相同,只差在 isFallingStone() 的回傳值和 moveHorizontal() 的行為。這種「只差一組常數方法」的情況,可以合併為單一 class。

做法(類似分數加法)#

第一階段:讓非基底方法相同

  1. 在每個 class 的 moveHorizontal 主體外加上 if (true) { }
  2. true 替換為基底方法的比較式(如 this.isFallingStone() === false
  3. 將另一個 class 的主體以 else 貼入,使兩邊的 moveHorizontal 完全一致

第二階段:合併 class

  1. 引入 falling 欄位,在 constructor 中賦予各自的常數值
  2. isFallingStone() 改為回傳 this.falling
  3. 逐步將常數改為 constructor 參數
  4. 刪除其中一個 class,把所有實例化導向保留的 class

合併後的 Stone 接受 falling 參數,隱藏的 type code 被暴露:布林值 falling 就是一個 type code。

進一步處理暴露出的 type code#

  1. boolean 改為 enum FallingState { FALLING, RESTING }
  2. 再用 REPLACE TYPE CODE WITH CLASSES 把 enum 轉為 interface + Falling / Resting class
  3. PUSH CODE INTO CLASSESmoveHorizontal 中的邏輯推入 FallingResting
  4. TRY DELETE THEN COMPILE 清除不再使用的 isResting() 等方法

同樣的流程也適用於 Box / FallingBox,而且可以重用 FallingState interface。

flowchart TD
    subgraph phase1["第一階段:讓方法相同"]
        A["各 class 方法外\n加上 if(true)"] --> B["替換為基底方法\n比較式"]
        B --> C["將另一 class 主體\n以 else 貼入"]
    end
    subgraph phase2["第二階段:合併 class"]
        D["引入欄位\nconstructor 賦常數值"] --> E["基底方法改為\n回傳欄位"]
        E --> F["常數改為\nconstructor 參數"]
        F --> G["刪除其中一個 class"]
    end
    phase1 --> phase2
    subgraph phase3["進一步處理 type code"]
        H["boolean → enum"] --> I["REPLACE TYPE CODE\nWITH CLASSES"]
        I --> J["PUSH CODE\nINTO CLASSES"]
        J --> K["TRY DELETE\nTHEN COMPILE"]
    end
    phase2 -->|"暴露隱藏的\ntype code"| phase3

Refactoring Pattern: COMBINE IFS#

問題#

兩個相鄰的 else if 有完全相同的主體。

做法#

  1. 確認兩個主體完全一致
  2. 刪除第一個 if 的右括號到第二個 else if 的左括號之間的內容,插入 ||
  3. 在整個複合條件外加上括號以確保語義不變
  4. 移除多餘的括號

在 updateTile 上的應用#

引入 rest() 方法後,最後兩個 else ifisFallingStoneisFallingBox)的主體相同,合併為 isFallingStone() || isFallingBox(),再推入 class 成為 isFalling()


條件運算的算術規則 (Condition Arithmetic)#

|| 的行為類似加法 +&& 的行為類似乘法 x

Figure 5.1: Mnemonic to help remember precedence

這讓我們可以像操作數學算式一樣簡化條件式。例如:

A && C || B && C  →  (A || B) && C

Figure 5.2: Arithmetic rules

Figure 5.3: Applying arithmetic rules

透過這種轉換,updateTile 中的 map[y][x].isStony() && air || map[y][x].isBoxy() && air 被簡化為 (isStony() || isBoxy()) && air,再推入 class 成為 canFall()

上述算術規則僅在條件無副作用時才成立。這就是 USE PURE CONDITIONS 規則存在的原因。


Rule: USE PURE CONDITIONS#

  • 條件(ifwhile 後面的表達式)必須是純粹的 (pure)——不可賦值、不可拋例外、不可做 I/O
  • 有副作用的條件會阻礙算術規則的套用,也違反直覺(我們不預期條件會改變狀態)
  • 若無法控制實作(如 reader.readLine() 同時移動指標),可用 Cache 將副作用與回傳值分離

UML 類別圖入門#

類別圖用來描繪 class 與 interface 之間的結構關係,在說明 Design Pattern 時特別常用。

Figure 5.4: Class diagram

Figure 5.5: UML relations

常用的兩種關係#

關係符號語義
Composition實心菱形箭頭「A has a B」——A 持有 B 的實例作為欄位
Implementation空心三角箭頭 + 虛線「B implements A」——B 實作了 A interface

由於 ONLY INHERIT FROM INTERFACES 規則,繼承箭頭(實心三角)在本書中幾乎不會出現。

Figure 5.6: Implementation

Figure 5.7: Composition


Refactoring Pattern: INTRODUCE STRATEGY PATTERN#

問題#

StoneBox 中的 update 邏輯完全相同——掉落行為應該保持同步,且未來可能還有更多 Tile 需要此行為。這裡我們不希望 divergence(如第四章對 draw 的態度),而是希望 convergence。

做法#

  1. 用 EXTRACT METHOD 隔離要統一的邏輯
  2. 建立新 class(如 FallStrategy
  3. 在原 class 的 constructor 中實例化新 class
  4. 把方法移入新 class
  5. 若依賴原 class 的欄位,一併搬移欄位並建立 accessor
  6. 用參數取代新 class 中對 this 的引用
  7. 用 INLINE METHOD 反轉第一步的抽取
flowchart TD
    A["EXTRACT METHOD\n隔離要統一的邏輯"] --> B["建立新 class\n如 FallStrategy"]
    B --> C["在原 class constructor\n中實例化新 class"]
    C --> D["將方法移入新 class"]
    D --> E{"依賴原 class 欄位?"}
    E -->|是| F["搬移欄位\n建立 accessor"]
    E -->|否| G["用參數取代\nnew class 中的 this"]
    F --> G
    G --> H["INLINE METHOD\n反轉第一步的抽取"]

Figure 5.8: Class diagram with a focus on FallStrategy

Figure 5.9: Strategy pattern as a class diagram

Strategy Pattern 的意義#

  • 物件取代了 if,是 late binding 的終極形式
  • 在執行時期,甚至可以載入編譯時未知的 class,無縫整合到控制流中
  • 即便當下不需要變體,引入 Strategy 也為未來保留了擴展點

如果需要引入 variance,再透過 EXTRACT INTERFACE FROM IMPLEMENTATION 加上 interface。不需要的話就不加——遵循「不要有只有一個實作的 interface」規則。


Rule: NO INTERFACE WITH ONLY ONE IMPLEMENTATION#

  • 只有一個實作的 interface 不增加可讀性,反而發出虛假的「這裡有變體」訊號
  • 它徒增檔案數量與心智負擔
  • 等到真正需要 variance 時再用 EXTRACT INTERFACE FROM IMPLEMENTATION 建立 interface 即可

Refactoring Pattern: EXTRACT INTERFACE FROM IMPLEMENTATION#

步驟#

  1. 建立一個與 class 同名的新 interface
  2. 將原 class 重新命名,並讓它 implements 新 interface
  3. 編譯,處理所有錯誤:
    • new 語句改用新 class 名稱
    • 其餘報錯的方法加入 interface

應用#

removeLock1 / removeLock2 為例:先用 INTRODUCE STRATEGY PATTERN 將 isLock1() 的檢查抽成 RemoveStrategy class,再用 EXTRACT INTERFACE FROM IMPLEMENTATION 建立 RemoveStrategy interface 與 RemoveLock1 / RemoveLock2 兩個實作,最終合併為單一 remove(shouldRemove: RemoveStrategy) 函式。

同樣的技巧也用來合併 Key1/Key2Lock1/Lock2,引入 KeyConfiguration class 來統一管理顏色、鎖 ID、移除策略之間的關聯。

classDiagram
    class RemoveStrategy {
        <<interface>>
        +check(tile) bool
    }
    class RemoveLock1 {
        +check(tile) bool
    }
    class RemoveLock2 {
        +check(tile) bool
    }
    class KeyConfiguration {
        -color
        -lockId
        -removeStrategy: RemoveStrategy
        +removeLock()
        +setColor(g)
    }
    RemoveStrategy <|.. RemoveLock1
    RemoveStrategy <|.. RemoveLock2
    KeyConfiguration --> RemoveStrategy

本章小結#

  • UNIFY SIMILAR CLASSES 合併只差常數方法的 class,暴露隱藏的 type code 以便進一步處理
  • COMBINE IFS 合併主體相同的相鄰 if,暴露 || 關係以便推入 class
  • 條件算術規則讓我們像簡化數學式一樣簡化條件式,前提是遵守 USE PURE CONDITIONS
  • UML 類別圖幫助我們以圖形方式理解 class 關係
  • INTRODUCE STRATEGY PATTERN 是本書最精巧的重構模式,用獨立的 class 統一跨 class 的重複邏輯
  • NO INTERFACE WITH ONLY ONE IMPLEMENTATION 避免不必要的抽象;需要時再用 EXTRACT INTERFACE FROM IMPLEMENTATION 建立 interface