Day 13:JavaScript - TDD 與模組化重構

本文整理第一次 Code review 後第一次程式碼重構的歷程,歸納核心觀念與模組化架構設計,作為後續 Node.js 專案開發的基礎參考。


1. 架構設計思維

1.1 責任分層(Separation of Concerns)

依責任將程式碼切分為三層,每一層只關注自己的任務:

  1. 驗證層(Validation Layer)

    • 職責:定義資料的合法性規則(型別、格式、範圍),不涉及業務邏輯或執行環境。

    • 特性:純函式(Pure Function),無副作用,可被任何模組(CLI、核心邏輯、前端)重用。

    • 例子:validate.js 裡的 parseNum、parseNonNegativeInt。

  2. 核心邏輯層(Core Domain Layer)

    • 職責:處理業務規則(例如「計算訂單總價」、「更新使用者狀態」),產生運算結果。

    • 特性:依賴驗證層進行防禦,但不依賴執行環境(如 readline、DOM)。

    • 設計:可選擇在入口保留防禦式檢查,讓核心邏輯在被其他模組直接呼叫時仍具備保護能力。

    • 例子:order.js 的 calculateTotal。

  3. 執行層 / 互動層(Execution / Interaction Layer)

    • 職責:處理與使用者的互動(輸入/輸出),串接驗證層與核心邏輯層。

    • 特性:高度依賴特定環境 API(如 Node.js readline/promises),負責流程控制(迴圈、重試)。

    • 例子:各功能的 main.js,以及 CLI 封裝工具 cliPrompt.js。

1.2 模組相依性原則

建立清晰、單向的相依關係,避免循環依賴與混亂:

  • 相依方向大致為:

    執行層 main.js
        ↓
    CLI 工具 cliPrompt.js / readline.js
        ↓
    驗證層 validate.js   ←  核心邏輯層 order.js
  • 原則:

    • 底層模組(如 validate.js)不應依賴上層模組。

    • CLI 相關模組(readline.js, cliPrompt.js)不應反向依賴業務邏輯模組。

    • 相對路徑匯入時,於 ES module 環境要使用完整路徑+.js 副檔名,例如:../util/validate.js。


2. 檔案結構規劃

採用 util/ 目錄集中通用工具,各功能資料夾(feature-a/, feature-b/)專注於該功能的業務邏輯與執行入口:

root/
├── util/ # 通用工具庫
│ ├── validate.js # 純驗證函式(Parse & Check)
│ ├── validate.test.js # 驗證函式的單元測試
│ ├── readline.js # Node.js Readline 封裝(Factory Pattern)
│ └── cliPrompt.js # CLI 互動封裝(問答、驗證、重試迴圈)
│
├── feature-a/ # 功能 A:訂單處理
│ ├── order.js # 核心邏輯(Domain Logic)
│ ├── order.test.js # 核心邏輯測試
│ └── main.js # 執行入口(CLI Entry Point)
│
├── feature-b/ # 功能 B:庫存更新
│ ├── stock.js
│ ├── stock.test.js
│ └── main.js
│
└── package.json

此結構的意義:

  • util/ 保留為可重用工具庫,避免與特定業務邏輯糾纏。

  • 每個功能資料夾完整擁有「核心邏輯+測試+執行入口」,方便獨立開發與重構。


3. 驗證模組與測試策略

3.1 驗證函式分層

驗證模組 validate.js 採取由下而上的組合式設計:

  1. parseNum

    • 輸入:任意值。

    • 行為:

      • 轉字串,trim 空白。

      • 使用正規表達式 /^[0-9]+$/ 確認為純數字且至少一位。

      • 使用 parseInt(str, 10) 轉成十進位整數。

    • 若格式不符合,丟出 Error('輸入錯誤')。

  2. parseNonNegativeInt

    • 輸入:任意值。

    • 行為:呼叫 parseNum,再檢查 num >= 0。

    • 若小於 0,丟出 Error('輸入錯誤')。

  3. parsePositiveInt

    • 輸入:任意值。

    • 行為:呼叫 parseNum,再檢查 num > 0。

    • 若小於等於 0,丟出 Error('輸入錯誤')。

此設計讓「型別/格式驗證」與「數值範圍規則」可自由組合、重用。

3.2 TDD 測試策略

validate.test.js 的測試規劃遵守以下原則:

  • 每個驗證函式至少具備:

    • 合法輸入案例:確認回傳值正確。

    • 非法輸入案例:確認會丟出預期錯誤(例:'abc'、'-1'、'0' 對正整數函式而言)。

  • 對丟錯的測試一律採用:

    expect(() => someValidator(badInput)).toThrow('輸入錯誤');
    
  • 一旦驗證模組測試綠燈,後續核心邏輯與 CLI 僅需專注自身行為,出現異常時也能快速定位是驗證層還是核心層的問題。


4. CLI 互動封裝模式

4.1 Readline 工廠函式

readline.js 封裝 Node.js 的 readline/promises:

  • 每次呼叫 createRl() 建立新的 Interface。

  • 在關閉後(rl.close())不再使用舊實例,避免 ERR_USE_AFTER_CLOSE 類錯誤。

此設計允許在 while 迴圈中反覆「關閉舊介面、建立新介面」,實作多輪輸入與重試。

4.2 通用 CLI 問答工具 askWithValidator

cliPrompt.js 抽象出「問一題+驗證+錯誤重試」的固定模式:

  • 參數:

    • prompt: 要顯示的提示字串。

    • validator: 驗證函式(例如 parseNum、parseNonNegativeInt)。

  • 邏輯:

    • 使用 while 迴圈反覆提問。

    • 每次輸入交給 validator 檢查:

      • 驗證通過 → 回傳解析後的值,結束迴圈。

      • 驗證失敗 → 顯示錯誤訊息,重新建立 readline Interface,進入下一輪提問。

此工具讓「互動流程」與「驗證規則」實現鬆耦合,業務端只需替換驗證函式即可復用一整套輸入流程。


5. 應用範例:重構兩個獨立功能

將上述架構應用於兩個常見的功能開發場景。

5.1 功能 A:訂單數量處理

此功能需要使用者輸入一個正整數作為訂單數量。

  • CLI 執行程式碼:透過 askWithValidator('請輸入訂單數量:', parsePositiveInt) 取得合法的正整數。

  • 核心函式 processOrder(quantity):在已驗證前提下,處理業務邏輯。

    // feature-a/order.js
    export function processOrder(quantity) {
      // 此處已假設 quantity 為正整數,直接處理業務
      return `訂單已接收,數量:${quantity}`;
    }

5.2 功能 B:商品庫存驗證

此功能需要使用者輸入商品 ID(正整數)與庫存數量(非負整數)。

  • CLI 執行程式碼:分別呼叫 askWithValidator 兩次,並傳入不同的驗證函式。

    const productId = await askWithValidator('請輸入商品 ID:', parsePositiveInt);
    const stockLevel = await askWithValidator('請輸入庫存數量:', parseNonNegativeInt);
  • 核心函式 updateStock(productId, stockLevel):內部再次呼叫驗證函式做防禦,確保收到的參數符合預期。

    // feature-b/stock.js
    import { parsePositiveInt, parseNonNegativeInt } from '../util/validate.js';
    
    export function updateStock(productId, stockLevel) {
      const validProductId = parsePositiveInt(productId);
      const validStockLevel = parseNonNegativeInt(stockLevel);
    
      return `商品 ${validProductId} 的庫存已更新為 ${validStockLevel}`;
    }

在此結構下,updateStock 的單元測試會涵蓋「productId 為 0 或負數」以及「stockLevel 為負數」等非法情境,間接確保了驗證模組被正確依賴。


6. 重構效益與收穫

  1. 高可重用性:驗證邏輯與 CLI 互動邏輯被抽離,新功能只需專注於核心演算法與特定的驗證規則組合。

  2. 高可維護性:修改驗證規則(例如定義何謂整數)只需修改 validate.js 一處,所有功能同步更新。

  3. 測試清晰:單元測試各司其職,validate.test.js 測格式,order.test.js 測業務規則,除錯範圍明確。

  4. 防禦式程式設計:透過「UI/CLI 即時攔截」與「Domain 最終防禦」兩層保護,讓程式碼更健壯。


參考文件