本章核心#
本章從最根本的規則 FIVE LINES 出發,帶出第一個重構模式 EXTRACT METHOD,並延伸出三條輔助規則:EITHER CALL OR PASS、IF ONLY AT THE START,以及 NEVER USE if WITH else。目標是把冗長函式拆散成各自只做一件事的小函式,同時以方法名稱取代註解。
Rule: FIVE LINES — 方法不超過五行#
- 一行指的是
if、for、while,或以分號結尾的語句(賦值、方法呼叫、return等) - 空白行與大括號
{}不計入 - 五行的極限來自「遍歷基本資料結構所需的最少行數」;書中以 2D 陣列為基本結構,恰好五行就能做一次完整走訪
- 四個五行方法比一個二十行方法更容易理解——每個方法名稱本身就是一條註解
具體數字不是重點,重點在於「有上限」。實務中可依情境微調,但經驗上最終幾乎都會回到五行左右。
為什麼需要這條規則#
- 方法會隨時間不斷膨脹,最終變得難以理解
- 強制限制行數,等同於強制讓每個方法只做一件事(Methods should do one thing)
- 小方法更容易取好名字;而好名字反過來讓大函式的命名也更清楚
Refactoring Pattern: EXTRACT METHOD#
基本做法#
- 用空行與暫時註解,在目標函式中標示出「一組相關行」
- 建立一個新的空方法,名稱取自註解
- 在原來的位置放一個呼叫新方法的語句
- 把該組行剪下,貼入新方法的主體
- 編譯——缺少的變數會以參數方式補齊
- 若新方法中有對參數賦值(例如
result = ...),則加上return並在呼叫端接收回傳值 - 刪除已失效的空行與註解
flowchart TD
A["標示相關行\n用空行與註解分組"] --> B["建立新的空方法\n名稱取自註解"]
B --> C["在原位放一個呼叫語句"]
C --> D["剪下相關行\n貼入新方法"]
D --> E{"編譯:缺少變數?"}
E -->|是| F["以參數方式補齊"]
E -->|否| G{"有對參數賦值?"}
F --> G
G -->|是| H["加上 return\n呼叫端接收回傳值"]
G -->|否| I["刪除空行與註解"]
H --> I在 draw 上的實作#
原始的 draw 函式內有兩段用註解分隔的邏輯群組:// Draw map 與 // Draw player。

Figure 3.1: Initial draw function
依照上述步驟,將兩段分別抽成 drawMap(g) 與 drawPlayer(g),並以函式名稱取代原本的註解。

Figure 3.2: Before

Figure 3.3: After
整個過程只是「搬動程式碼」,編譯器會告訴我們遺漏了哪些參數,因此引入 bug 的風險極低。
Pro tip#
- 若
return只出現在某些分支而阻礙抽取,建議從方法底部往上開始抽取,這樣return會被逐步推往上方,最終出現在所有分支中
Rule: EITHER CALL OR PASS — 呼叫或傳遞,不要兩者兼做#
- 一個函式要嘛在某物件上呼叫方法,要嘛把該物件當參數傳出去,不應同時進行兩者
- 違反此規則意味著函式混合了不同的抽象層級——一邊做低階操作(如陣列索引),一邊呼叫高階方法

Figure 3.4: g being both passed and called
修正方式#
以 draw 為例:變數 g 既被呼叫 g.clearRect(),又被傳給 drawMap(g) 和 drawPlayer(g)。解法是把前三行一起抽成 createGraphics(),讓 draw 變成:
function draw() {
let g = createGraphics();
drawMap(g);
drawPlayer(g);
}所有行都在同一抽象層級上——只呼叫方法,不直接操作物件。
良好方法名稱的三個特性#
| 特性 | 英文 | 說明 |
|---|---|---|
| 誠實 | honest | 描述函式的意圖 |
| 完整 | complete | 涵蓋函式做的所有事 |
| 可理解 | understandable | 使用領域內的詞彙 |
Rule: IF ONLY AT THE START — if 只放在函式開頭#
- 檢查條件本身就是「一件事」;如果函式裡有
if,它應該是函式的第一件(也是唯一的一件)事 if連同整條else if鏈視為一個原子單位,不可再拆分- 如果
if出現在函式中間,代表這個函式至少做了兩件事

Figure 3.5: if in the middle of a function
修正方式#
以 updateMap 為例:for 迴圈中間有一大段 if-else if 鏈。把這段抽成 updateTile(x, y),updateMap 就回到五行以內。同理,handleInputs 中的 if-else if 鏈也被抽成 handleInput(input)。
Extract Method 還有一個附帶好處——可以在新方法中重新命名參數,例如迴圈中的
current在新函式裡更適合叫input。
Rule: NEVER USE if WITH else — 用多型取代 if-else#
if-else是一種硬編碼決策 (hardcoded decision),等同 early binding,鎖死了決策點- 應只在程式邊界(處理外部輸入等不可控資料型別時)才使用
if-else - 在核心邏輯中,用物件與多型(late binding)取代
if-else,這樣就能透過新增類別來擴展行為,而非修改現有的if語句
初步方向#
- 將 enum 透過 REPLACE TYPE CODE WITH CLASSES 轉為 interface + 各值對應的 class
- 再用 PUSH CODE INTO CLASSES 把
if-else中的邏輯推進各 class - 最終
if-else消失,每個 class 只包含自己的行為
flowchart LR
A["enum\n(type code)"] -->|"REPLACE TYPE CODE\nWITH CLASSES"| B["interface +\n各值對應的 class"]
B -->|"PUSH CODE\nINTO CLASSES"| C["各 class\n包含自己的行為"]
C --> D["if-else 消失"]本章只點出這條規則,具體的替換手法在第四章(Make Type Codes Work)才會展開。
本章小結#
- FIVE LINES 規則幫助識別「做太多事」的方法;用 EXTRACT METHOD 拆解之,並以方法名稱取代註解
- EITHER CALL OR PASS 規則確保函式內部維持一致的抽象層級,再次透過 EXTRACT METHOD 修正
- 良好的方法名稱應誠實、完整且可理解;EXTRACT METHOD 也允許我們重新命名參數以提高可讀性
- IF ONLY AT THE START 規則隔離條件檢查,確保每個函式只做一件事
- NEVER USE if WITH else 規則預告了後續章節用多型取代條件判斷的方向