本章核心#

本章從最根本的規則 FIVE LINES 出發,帶出第一個重構模式 EXTRACT METHOD,並延伸出三條輔助規則:EITHER CALL OR PASSIF ONLY AT THE START,以及 NEVER USE if WITH else。目標是把冗長函式拆散成各自只做一件事的小函式,同時以方法名稱取代註解。


Rule: FIVE LINES — 方法不超過五行#

  • 一行指的是 ifforwhile,或以分號結尾的語句(賦值、方法呼叫、return 等)
  • 空白行與大括號 { } 不計入
  • 五行的極限來自「遍歷基本資料結構所需的最少行數」;書中以 2D 陣列為基本結構,恰好五行就能做一次完整走訪
  • 四個五行方法比一個二十行方法更容易理解——每個方法名稱本身就是一條註解

具體數字不是重點,重點在於「有上限」。實務中可依情境微調,但經驗上最終幾乎都會回到五行左右。

為什麼需要這條規則#

  • 方法會隨時間不斷膨脹,最終變得難以理解
  • 強制限制行數,等同於強制讓每個方法只做一件事(Methods should do one thing
  • 小方法更容易取好名字;而好名字反過來讓大函式的命名也更清楚

Refactoring Pattern: EXTRACT METHOD#

基本做法#

  1. 用空行與暫時註解,在目標函式中標示出「一組相關行」
  2. 建立一個新的空方法,名稱取自註解
  3. 在原來的位置放一個呼叫新方法的語句
  4. 把該組行剪下,貼入新方法的主體
  5. 編譯——缺少的變數會以參數方式補齊
  6. 若新方法中有對參數賦值(例如 result = ...),則加上 return 並在呼叫端接收回傳值
  7. 刪除已失效的空行與註解
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 CLASSESif-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 規則預告了後續章節用多型取代條件判斷的方向