本章核心#
上一章結尾留下了一個無法用 EXTRACT METHOD 處理的 handleInput——因為 else if 鏈不能被拆開。本章引入 NEVER USE if WITH else 與 NEVER USE switch 兩條規則,並用 REPLACE TYPE CODE WITH CLASSES 和 PUSH CODE INTO CLASSES 兩個重構模式來消除條件判斷。此外還介紹了移除不必要泛化的 SPECIALIZE METHOD、反向收拾的 INLINE METHOD 以及清理死碼的 TRY DELETE THEN COMPILE。
Rule: NEVER USE if WITH else#
if-else是 early binding:決策在編譯時被固定,無法在不修改if語句的情況下新增行為- 若檢查的是「我們無法控制的資料型別」(如外部 I/O 的
string、KeyboardEvent),則允許使用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 打下基礎。
步驟#
- 建立一個暫時名稱的新 interface(如
Input2),包含對應 enum 各值的isXxx()方法 - 為每個 enum 值建立一個 class,只有對應的
isXxx()回傳true,其餘回傳false - 將 enum 重新命名(如
RawInput),讓編譯器在所有使用處報錯 - 把型別從舊名改為新 interface,並將相等性檢查
=== Input.LEFT替換為方法呼叫input.isLeft() - 把剩餘的 enum 值參照改為
new Left()等實例化語句 - 所有錯誤修完後,將暫時的
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#
任何支援精確相等比較 === 的型別(如 int、string 常數)都可能扮演 type code。遇到數字型的 type code,應先轉為 enum,再套用此重構模式。
Refactoring Pattern: PUSH CODE INTO CLASSES#
目的#
將函式中的 if-else 邏輯推入各 class,消除條件判斷,讓功能靠近資料。
步驟#
- 把整個來源函式複製到所有 class 中,移除
function關鍵字,把參數替換為this,先給一個略微不同的暫時名稱 - 在 interface 中加入新方法的簽名
- 在每個 class 中:
- 內聯
isXxx()方法的回傳值(替換為true或false) - 移除
if (false) { ... }區塊、拆掉if (true)的外殼 - 改回正式名稱,讓編譯器接受
- 內聯
- 把原始函式的主體替換為呼叫新方法
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()),保留它才符合「同一抽象層級」的原則
步驟#
- 將方法暫時重新命名(引發編譯錯誤)
- 複製方法主體,記下參數
- 在每個報錯處,用主體替換呼叫,將引數對應到參數
- 確認編譯通過後,刪除原方法
處理更大的 if:drawMap 與 Tile enum#
drawMap 中有一段巨大的 else if 鏈(colorOfTile)和多處比較 map[y][x] === Tile.XXX。處理流程:
| 步驟 | 模式 | 操作 |
|---|---|---|
| 1 | EXTRACT METHOD | 先把 else if 鏈抽成 colorOfTile |
| 2 | REPLACE TYPE CODE WITH CLASSES | 把 Tile enum 轉為 interface + 12 個 class |
| 3 | PUSH CODE INTO CLASSES | 把 colorOfTile 的邏輯推入各 Tile class 的 color() 方法 |
| 4 | INLINE METHOD | colorOfTile 只剩一行,內聯後刪除 |
| 5 | PUSH CODE INTO CLASSES | 把 drawMap 中剩餘的繪圖邏輯推入各 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,且每個case都return default會讓編譯器無法在新增 enum 值時提醒你處理新情況- fall-through 邏輯容易遺漏
break,造成 bug
switch 讓我們「按照位置」(context) 決定每個值的行為;而 class 讓我們「按照資料」(data) 組織行為。前者全域化了不變量,後者局部化了不變量。
Refactoring Pattern: SPECIALIZE METHOD#
- 當方法「過度泛化」阻礙了重構時,建立更特化的版本
- 例如
remove(tile: Tile)在實際使用中只移除Lock1或Lock2,因此拆成removeLock1()和removeLock2() - 特化後的方法呼叫點更少,更容易在未來被內聯或刪除
步驟#
- 複製目標方法
- 將其中一份重新命名,移除(或替換)要特化的參數
- 修正方法使其無錯誤
- 將舊的呼叫切換為新的特化版本
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 else 與 NEVER USE switch 規則要求我們在核心邏輯中避免低階控制流,改用 class 與多型
- REPLACE TYPE CODE WITH CLASSES 將 enum 值轉為 class,PUSH CODE INTO CLASSES 消除
if-else,兩者是本書最核心的重構組合 - SPECIALIZE METHOD 解決過度泛化阻礙重構的問題
- ONLY INHERIT FROM INTERFACES 避免繼承帶來的緊耦合
- INLINE METHOD 與 TRY DELETE THEN COMPILE 負責善後——移除不再有意義的方法與死碼