# Day 13:JavaScript - TDD 與模組化重構 **Published by:** [雞蛋糕的前端修煉屋](https://paragraph.com/@gcake/) **Published on:** 2025-12-08 **URL:** https://paragraph.com/@gcake/day-13 ## Content 本文整理第一次 Code review 後第一次程式碼重構的歷程,歸納核心觀念與模組化架構設計,作為後續 Node.js 專案開發的基礎參考。1. 架構設計思維1.1 責任分層(Separation of Concerns)依責任將程式碼切分為三層,每一層只關注自己的任務:驗證層(Validation Layer)職責:定義資料的合法性規則(型別、格式、範圍),不涉及業務邏輯或執行環境。特性:純函式(Pure Function),無副作用,可被任何模組(CLI、核心邏輯、前端)重用。例子:validate.js 裡的 parseNum、parseNonNegativeInt。核心邏輯層(Core Domain Layer)職責:處理業務規則(例如「計算訂單總價」、「更新使用者狀態」),產生運算結果。特性:依賴驗證層進行防禦,但不依賴執行環境(如 readline、DOM)。設計:可選擇在入口保留防禦式檢查,讓核心邏輯在被其他模組直接呼叫時仍具備保護能力。例子:order.js 的 calculateTotal。執行層 / 互動層(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 採取由下而上的組合式設計:parseNum輸入:任意值。行為:轉字串,trim 空白。使用正規表達式 /^[0-9]+$/ 確認為純數字且至少一位。使用 parseInt(str, 10) 轉成十進位整數。若格式不符合,丟出 Error('輸入錯誤')。parseNonNegativeInt輸入:任意值。行為:呼叫 parseNum,再檢查 num >= 0。若小於 0,丟出 Error('輸入錯誤')。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 問答工具 askWithValidatorcliPrompt.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. 重構效益與收穫高可重用性:驗證邏輯與 CLI 互動邏輯被抽離,新功能只需專注於核心演算法與特定的驗證規則組合。高可維護性:修改驗證規則(例如定義何謂整數)只需修改 validate.js 一處,所有功能同步更新。測試清晰:單元測試各司其職,validate.test.js 測格式,order.test.js 測業務規則,除錯範圍明確。防禦式程式設計:透過「UI/CLI 即時攔截」與「Domain 最終防禦」兩層保護,讓程式碼更健壯。參考文件Number.isNaN() - JavaScript | MDNparseInt() - JavaScript | MDN正規表達式 - JavaScript 指南 | MDNControl flow and error handling - JavaScript | MDNReadline | Node.js Documentationexpect API | Vitest ## Publication Information - [雞蛋糕的前端修煉屋](https://paragraph.com/@gcake/): Publication homepage - [All Posts](https://paragraph.com/@gcake/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@gcake): Subscribe to updates