本章核心#

上一章結尾留下了一個無法用 EXTRACT METHOD 處理的 handleInput——因為 else if 鏈不能被拆開。本章引入 NEVER USE if WITH elseNEVER USE switch 兩條規則,並用 REPLACE TYPE CODE WITH CLASSESPUSH CODE INTO CLASSES 兩個重構模式來消除條件判斷。此外還介紹了移除不必要泛化的 SPECIALIZE METHOD、反向收拾的 INLINE METHOD 以及清理死碼的 TRY DELETE THEN COMPILE


Rule: NEVER USE if WITH else#

  • if-elseearly binding:決策在編譯時被固定,無法在不修改 if 語句的情況下新增行為
  • 若檢查的是「我們無法控制的資料型別」(如外部 I/O 的 stringKeyboardEvent),則允許使用 else if;這類判斷通常出現在程式邊界,負責把外部型別映射到我們自己的型別
  • 獨立的 if(不帶 else)屬於檢查 (check) 而非決策 (decision),用於方法開頭的前置驗證是可以接受的

Late Binding 的優勢#

物件導向提供了比 if-else 更強大的控制流工具——物件。透過 interface 與多個 implementation,我們可以在執行時期根據實例化的 class 決定要跑哪段程式碼,實現 change by addition


Refactoring Pattern: REPLACE TYPE CODE WITH CLASSES#

目的#

將 enum 轉換為 interface + classes,讓每個值成為獨立物件,為後續 PUSH CODE INTO CLASSES 打下基礎。

步驟#

  1. 建立一個暫時名稱的新 interface(如 Input2),包含對應 enum 各值的 isXxx() 方法
  2. 為每個 enum 值建立一個 class,只有對應的 isXxx() 回傳 true,其餘回傳 false
  3. 將 enum 重新命名(如 RawInput),讓編譯器在所有使用處報錯
  4. 把型別從舊名改為新 interface,並將相等性檢查 === Input.LEFT 替換為方法呼叫 input.isLeft()
  5. 把剩餘的 enum 值參照改為 new Left() 等實例化語句
  6. 所有錯誤修完後,將暫時的 Input2 重新命名為 Input
flowchart TD
    A["建立暫時名稱的新 interface\n如 Input2"] --> B["為每個 enum 值\n建立對應 class"]
    B --> C["重新命名 enum\n觸發編譯錯誤"]
    C --> D["將型別改為新 interface\n替換 === 為 .isXxx()"]
    D --> E["將 enum 值參照\n改為 new Class()"]
    E --> F["將暫時名稱\n重新命名為正式名稱"]

此重構本身價值有限——它只是「用一個 smell 換了另一個 smell」。但它把原本緊密耦合的 enum 值拆開成獨立的 class,讓我們可以逐一處理 isXxx() 方法。

Type code 不只是 enum#

任何支援精確相等比較 === 的型別(如 intstring 常數)都可能扮演 type code。遇到數字型的 type code,應先轉為 enum,再套用此重構模式。


Refactoring Pattern: PUSH CODE INTO CLASSES#

目的#

將函式中的 if-else 邏輯推入各 class,消除條件判斷,讓功能靠近資料。

步驟#

  1. 把整個來源函式複製到所有 class 中,移除 function 關鍵字,把參數替換為 this,先給一個略微不同的暫時名稱
  2. 在 interface 中加入新方法的簽名
  3. 在每個 class 中:
    • 內聯 isXxx() 方法的回傳值(替換為 truefalse
    • 移除 if (false) { ... } 區塊、拆掉 if (true) 的外殼
    • 改回正式名稱,讓編譯器接受
  4. 把原始函式的主體替換為呼叫新方法
flowchart TD
    A["複製來源函式到所有 class\n移除 function 關鍵字"] --> B["在 interface\n加入新方法簽名"]
    B --> C["各 class:內聯 isXxx()\n替換為 true 或 false"]
    C --> D["各 class:移除 if(false) 區塊\n拆掉 if(true) 外殼"]
    D --> E["各 class:改回正式名稱"]
    E --> F["原始函式主體\n替換為呼叫新方法"]

範例:handleInput 的改造#

透過上述步驟,原本的 handleInput 變成一行 input.handle(),而各 class 內部只剩各自的行為:

class Right implements Input {
  handle() {
    moveHorizontal(1);
  }
}
class Left implements Input {
  handle() {
    moveHorizontal(-1);
  }
}

這是作者最喜歡的重構模式——步驟極度機械化,認知負擔低,但產出的程式碼非常優雅。


Refactoring Pattern: INLINE METHOD#

  • 當一個方法經過重構後只剩一行且不再增加可讀性,就可以反向操作:將其主體內聯到所有呼叫端,然後刪除原方法
  • 不要內聯低階操作方法——如果方法封裝了低階細節(例如位元運算實現的 absolute()),保留它才符合「同一抽象層級」的原則

步驟#

  1. 將方法暫時重新命名(引發編譯錯誤)
  2. 複製方法主體,記下參數
  3. 在每個報錯處,用主體替換呼叫,將引數對應到參數
  4. 確認編譯通過後,刪除原方法

處理更大的 if:drawMap 與 Tile enum#

drawMap 中有一段巨大的 else if 鏈(colorOfTile)和多處比較 map[y][x] === Tile.XXX。處理流程:

步驟模式操作
1EXTRACT METHOD先把 else if 鏈抽成 colorOfTile
2REPLACE TYPE CODE WITH CLASSESTile enum 轉為 interface + 12 個 class
3PUSH CODE INTO CLASSEScolorOfTile 的邏輯推入各 Tile class 的 color() 方法
4INLINE METHODcolorOfTile 只剩一行,內聯後刪除
5PUSH CODE INTO CLASSESdrawMap 中剩餘的繪圖邏輯推入各 class 的 draw() 方法
flowchart LR
    A["drawMap\nelse if 鏈"] -->|"EXTRACT\nMETHOD"| B["colorOfTile"]
    B -->|"REPLACE TYPE CODE\nWITH CLASSES"| C["Tile interface\n+ 12 個 class"]
    C -->|"PUSH CODE\nINTO CLASSES"| D["各 class\ncolor()"]
    D -->|"INLINE\nMETHOD"| E["刪除\ncolorOfTile"]
    E -->|"PUSH CODE\nINTO CLASSES"| F["各 class\ndraw()"]

唯一允許的 switch#

將 enum 索引(如地圖資料中的數字)對應到新 class 時,需要一個 transformTile 函式。此 switch 沒有 default(改用 assertExhausted 技巧)且每個 case 都 return,符合規則例外。


Rule: NEVER USE switch#

  • 不使用 switch,除非同時滿足:沒有 default,且每個 casereturn
  • default 會讓編譯器無法在新增 enum 值時提醒你處理新情況
  • fall-through 邏輯容易遺漏 break,造成 bug

switch 讓我們「按照位置」(context) 決定每個值的行為;而 class 讓我們「按照資料」(data) 組織行為。前者全域化了不變量,後者局部化了不變量。


Refactoring Pattern: SPECIALIZE METHOD#

  • 當方法「過度泛化」阻礙了重構時,建立更特化的版本
  • 例如 remove(tile: Tile) 在實際使用中只移除 Lock1Lock2,因此拆成 removeLock1()removeLock2()
  • 特化後的方法呼叫點更少,更容易在未來被內聯或刪除

步驟#

  1. 複製目標方法
  2. 將其中一份重新命名,移除(或替換)要特化的參數
  3. 修正方法使其無錯誤
  4. 將舊的呼叫切換為新的特化版本

Rule: ONLY INHERIT FROM INTERFACES#

  • 只從 interface 繼承,不從 class 或 abstract class 繼承
  • abstract class 中的共享程式碼會造成耦合:子類別可能只需要部分方法,被迫繼承不需要的行為
  • 有預設實作的方法在新增子類別時不會由編譯器提醒,容易遺漏
  • 若多個 class 需要共享程式碼,應使用組合 (composition) 而非繼承(第五章的 Strategy Pattern)

Refactoring Pattern: TRY DELETE THEN COMPILE#

  • 用於移除 interface 中可能已未使用的方法
  • 步驟:確認編譯通過 → 刪除 interface 中的一個方法 → 編譯 → 有錯就復原,無錯就接著從各 class 中也刪除
  • 在完成一輪重構後立即清理,避免過時程式碼拖慢開發與理解速度

不要在「正在開發新功能」時執行此模式,否則可能刪除尚未被使用但即將需要的方法。


本章小結#

  • NEVER USE if WITH elseNEVER USE switch 規則要求我們在核心邏輯中避免低階控制流,改用 class 與多型
  • REPLACE TYPE CODE WITH CLASSES 將 enum 值轉為 class,PUSH CODE INTO CLASSES 消除 if-else,兩者是本書最核心的重構組合
  • SPECIALIZE METHOD 解決過度泛化阻礙重構的問題
  • ONLY INHERIT FROM INTERFACES 避免繼承帶來的緊耦合
  • INLINE METHODTRY DELETE THEN COMPILE 負責善後——移除不再有意義的方法與死碼