# Day 34:驗證器分層架構設計;While loop 時間序列狀態模擬 **Published by:** [雞蛋糕的前端修煉屋](https://paragraph.com/@gcake/) **Published on:** 2026-01-08 **URL:** https://paragraph.com/@gcake/day-34 ## Content 日期:2026/01/08 主題:驗證器架構設計、While Loop 狀態模擬、測試職責分離🎯 今日技術重點1. 驗證器架構設計✅ Predicate vs Validator 分層架構✅ 工廠函式(Factory Function)實踐✅ 命名慣例統一(is* vs ensure*)2. 演算法實作✅ While Loop 狀態模擬(時間序列計算)✅ 模組重用模式✅ 函數命名重構3. TDD 流程實踐✅ Red-Green-Refactor 循環✅ 測試與業務邏輯分離✅ Mock API 設計🔧 技術決策記錄案例 1:Predicate vs Validator 分層設計核心概念釐清層級名稱職責回傳值範例底層Predicate(斷言函式)判斷真偽BooleanisPrime(7) => true上層Validator(驗證器)驗證 + 轉換{ value }ensurePositive(7) => { value: 7 }設計原則 :[1]Predicate 是純函式:無副作用,可被 Array.filter()、Array.find() 使用Validator 是包裝器:統一介面格式,支援鏈式組合可組合性:Predicate 可以被多個 Validator 重用工廠函式實作/** * 高階函式:將 Predicate 包裝成 Validator * @category Higher-Order Function, Factory Pattern, Wrapper */ export function predicateToValidator(predicate, errorMessage) { return (input) => { // 返回閉包 if (predicate(input)) { return { value: input }; } throw new Error(errorMessage); }; } // 使用範例:質數驗證器 const isPrime = (n) => { if (n < 2) return false; for (let i = 2; i <= Math.sqrt(n); i++) { if (n % i === 0) return false; } return true; }; export const ensurePrime = predicateToValidator( isPrime, '輸入必須是質數' ); // 使用 ensurePrime(7); // { value: 7 } ensurePrime(4); // Error: 輸入必須是質數 三重身份:高階函式:接受函式參數 + 返回函式工廠函式:批量製造客製化驗證器包裝器模式:增強 predicate 功能而不修改原函式底層機制:閉包(Closure)[2]predicateToValidator 返回的函式記住了傳入的 predicate 和 errorMessage每次調用都能存取這些封閉變數案例 2:命名慣例統一語意衝突問題前綴語意回傳值使用情境is*判斷/詢問Boolean條件判斷、陣列過濾ensure*確保/斷言{ value } 或拋錯輸入驗證、資料轉換to*型別轉換轉換後的值類型轉換重構範例// ❌ 前:語意混淆 export function isPositiveInt(input) { const num = parseInt(input, 10); if (!Number.isInteger(num) || num <= 0) { throw new Error('必須是正整數'); // is* 不該拋錯 } return { value: num }; // is* 不該回傳物件 } // ✅ 後:語意清晰 export function ensurePositiveInt(input, min = 1, max = Infinity) { const num = parseInt(input, 10); if (!Number.isInteger(num) || num < min || num > max) { throw new Error(`必須是 ${min} 到 ${max} 之間的正整數`); } return { value: num }; } // 配合的 Predicate(純判斷) export const isPositiveInt = (n) => Number.isInteger(n) && n > 0; // 使用分工 const numbers = [1, -2, 3.5, 4]; numbers.filter(isPositiveInt); // [1, 4] - 用於過濾 ensurePositiveInt("5", 1, 10); // { value: 5 } - 用於驗證輸入 重構成本:低(編輯器全域搜尋取代) 收益:語意清晰,符合 JavaScript 社群慣例[3][4]案例 3:While Loop vs 數學公式情境:計算「競賽者在有週期性規則下到達終點的時間」方案比較方案 A:數學公式(O(1))// 假設:每 5 秒後退 1 公尺,速度 20 m/s function calculateTimeWithFormula(distance, speed, interval, backward) { const netPerCycle = speed * interval - backward; const cycles = Math.floor(distance / netPerCycle); const remaining = distance - cycles * netPerCycle; return cycles * (interval + backward / speed) + remaining / speed; } 方案 B:While Loop 模擬(O(n))function calculateTimeWithLoop(distance, speed, interval, backward) { let time = 0; let currentDistance = 0; while (currentDistance < distance) { time += 1; currentDistance += speed; // 週期性規則 if (time % interval === 0) { currentDistance -= backward; } } return time; } 決策結果:選擇 While Loop ⭐理由 :[1]✅ 可讀性:程式碼像在說故事,逐秒模擬過程✅ 可擴展性:規則複雜化時易於修改✅ 學習價值:直接對應現實思維模式✅ 除錯容易:可在迴圈內加 console.log 追蹤狀態適用情境:學習階段(理解優先於效能)規則可能變動(需求不明確)複雜邏輯(數學公式難以推導)何時選數學公式:效能關鍵(處理百萬級資料)規則固定且簡單生產環境優化階段案例 4:測試 vs 業務邏輯的界線問題識別// ❌ 錯誤:在測試中寫業務邏輯 describe("Race Calculator", () => { it("計算休息時間", () => { const runner1Time = calculateFinishTime(1000, 0.28); const runner2Time = calculateFinishTime(1000, 20, 5, 1); const restTime = runner1Time - runner2Time; // ← 業務邏輯 expect(restTime).toBeGreaterThan(3400); }); }); 問題:測試應該驗證「calculateFinishTime 是否正確」但這段程式碼在「用工具函式解決問題」違反測試單一職責[5][1]正確分工// ✅ 測試:只驗證函數正確性 describe("calculateFinishTime", () => { it("無後退行為時的計算", () => { expect(calculateFinishTime(1000, 0.28)).toBe(3572); }); it("有後退行為時的計算", () => { expect(calculateFinishTime(1000, 20, 5, 1)).toBeCloseTo(51, 0); }); }); // ✅ 主程式:組合函數解決問題 function main() { const runner1Time = calculateFinishTime(1000, 0.28); const runner2Time = calculateFinishTime(1000, 20, 5, 1); const restTime = Math.floor(runner1Time - runner2Time); console.log(`最大休息時間:${restTime} 秒`); } 核心原則:層級職責輸出方式測試驗證工具函數正確性expect() 斷言主程式組合工具函數解決問題console.log() 結果💡 技術亮點實作1. 狀態計算模組/** * 計算跑者在特定時間點的狀態(含週期性規則) * @param {number} speed - 速度(公尺/秒) * @param {number} time - 經過時間(秒) * @param {number} interval - 後退間隔(秒,0 表示無後退) * @param {number} backward - 每次後退距離(公尺) * @returns {{ time: number, distance: number }} */ export function calculateRunnerState(speed, time, interval = 0, backward = 0) { const state = { time, distance: speed * time }; if (interval > 0 && backward > 0) { const backwardCount = Math.floor(time / interval); state.distance -= backwardCount * backward; state.time += (backwardCount * backward) / speed; // 後退耗時 } return state; } 設計重點:預設參數處理簡單情況(無後退)回傳物件包含 { time, distance }(狀態快照)Math.floor() 計算完整週期數[3]2. 完賽模擬模組(重用上層模組)import { calculateRunnerState } from './calculateRunnerState.js'; /** * 模擬跑者到達目標距離所需時間 * @param {number} target - 目標距離 * @param {number} speed - 速度 * @param {number} interval - 後退間隔 * @param {number} backward - 後退距離 * @returns {number} 總耗時(秒) */ export function calculateFinishTime(target, speed, interval = 0, backward = 0) { let time = 0; while (true) { time += 1; const state = calculateRunnerState(speed, time, interval, backward); if (state.distance >= target) { return state.time; // 回傳實際耗時(包含後退時間) } } } 模組重用模式 :[2]calculateFinishTime (上層) └── 呼叫 calculateRunnerState (下層) └── 每秒計算一次狀態 類似開源範例:React Hooks:useEffect 重用 useStateExpress Middleware:路由處理器重用驗證中介層3. 主程式(業務邏輯層)import { calculateFinishTime } from './calculateFinishTime.js'; async function main() { console.log('=== 競賽模擬器 ===\n'); const distance = 1000; const runner1Speed = 0.28; const runner2Speed = 20; const runner2Interval = 5; const runner2Backward = 1; const runner1Time = calculateFinishTime(distance, runner1Speed); const runner2Time = calculateFinishTime( distance, runner2Speed, runner2Interval, runner2Backward ); const timeDiff = Math.floor(runner1Time - runner2Time); console.log(`跑者 1 完賽:${runner1Time} 秒`); console.log(`跑者 2 完賽:${runner2Time} 秒`); console.log(`\n時間差:${timeDiff} 秒`); } main().catch((err) => { console.error('程式發生錯誤:', err); process.exit(1); }); 🧪 TDD 實踐案例Red-Green-Refactor 循環[5][1]🔴 Red → 寫測試 → 測試失敗(預期行為未實作) 🟢 Green → 實作功能 → 測試通過 🔄 Refactor → 優化程式碼 → 測試持續通過 案例:參數驗證的 TDD 流程Round 1: Red(寫測試)// ensurePositiveInt.test.js import { describe, it, expect } from 'vitest'; import { ensurePositiveInt } from './validators.js'; describe('ensurePositiveInt', () => { it('應拋錯當 min 參數無效', () => { expect(() => ensurePositiveInt(5, -1)).toThrow('min 參數錯誤'); expect(() => ensurePositiveInt(5, 0)).toThrow('min 參數錯誤'); expect(() => ensurePositiveInt(5, 1.5)).toThrow('min 參數錯誤'); }); }); 執行測試:❌ 失敗(功能未實作)Round 2: Green(實作)// validators.js export function ensurePositiveInt(input, min = 1, max = Infinity) { // ✅ 新增參數驗證 if (!Number.isInteger(min) || min < 1) { throw new Error('min 參數錯誤'); } if (max !== Infinity && (!Number.isInteger(max) || max < min)) { throw new Error('max 參數錯誤'); } // 原有邏輯... const num = parseInt(input, 10); if (!Number.isInteger(num) || num < min || num > max) { throw new Error(`必須是 ${min} 到 ${max} 之間的正整數`); } return { value: num }; } 執行測試:✅ 通過Round 3: Refactor(重構)// 抽取驗證邏輯 function validateRange(min, max) { if (!Number.isInteger(min) || min < 1) { throw new Error('min 參數錯誤'); } if (max !== Infinity && (!Number.isInteger(max) || max < min)) { throw new Error('max 參數錯誤'); } } export function ensurePositiveInt(input, min = 1, max = Infinity) { validateRange(min, max); // 重用驗證邏輯 const num = parseInt(input, 10); if (!Number.isInteger(num) || num < min || num > max) { throw new Error(`必須是 ${min} 到 ${max} 之間的正整數`); } return { value: num }; } 執行測試:✅ 持續通過🎓 關鍵學習總結1. 設計模式實踐模式應用效益Factory PatternpredicateToValidator 工廠函式批量生成驗證器Wrapper PatternValidator 包裝 Predicate統一介面Strategy PatternWhile Loop vs 數學公式彈性切換演算法Module Pattern分層模組設計可重用、可測試2. 命名的重要性// ❌ 不好的命名 calculator(20, 5, 5, 1) // 名詞,不清楚動作 isPositive(x) // 回傳 { value } // 語意不符 // ✅ 好的命名 calculateRunnerState(20, 5, 5, 1) // 動詞+名詞,清楚動作 ensurePositive(x) // 回傳 { value } // 語意一致 isPositive(x) // 回傳 Boolean // Predicate 用法 3. TDD 的「小步快跑」哲學[1]核心優勢:每次改動小 → 錯誤容易定位持續通過測試 → 信心累積可隨時重構 → 測試保護網📂 專案結構範例race-simulator/ ├── calculateRunnerState.js # 狀態計算(底層) ├── calculateRunnerState.test.js # 4 tests ├── calculateFinishTime.js # 完賽模擬(上層,重用底層) ├── calculateFinishTime.test.js # 5 tests └── main.js # 業務邏輯(組合上層模組) validators/ ├── predicates.js # 純判斷函式(isPrime, isEven...) ├── validators.js # 驗證器(ensurePositive, ensurePrime...) ├── factory.js # predicateToValidator 工廠 └── validators.test.js # 測試 設計原則:SRP:每個模組只做一件事DRY:重用已測試的模組分層:底層工具 → 上層組合 → 業務邏輯🔧 工具與技術棧測試框架:VitestNode.js API:readline/promises(Promise-based)程式碼風格:Airbnb JavaScript Style Guide模組系統:ES Modules (ESM)版本控制:Git(小步提交)📚 延伸學習資源JavaScript 核心概念MDN - Closures(閉包)[2]MDN - import 動態載入[2]ECMAScript 規範 - Modulo 運算子[6]測試驅動開發TDD: Red-Green-Refactor[5]The TDD Cycle[1]設計模式JavaScript Modulo Operator[3]Higher-Order Functions[7]💡 今日核心收穫Predicate vs Validator 分層:底層判斷邏輯與上層驗證介面分離工廠函式三重身份:高階函式 + 工廠模式 + 包裝器命名慣例:is* 回傳布林,ensure* 回傳物件或拋錯While Loop 思維:可讀性優於效能(學習階段)測試職責:驗證工具正確性,不包含業務邏輯模組重用:下層工具被上層組合,上層工具被主程式使用TDD 小步快跑:每次只改 5-10 行,快速驗證📊 測試結果範例✓ calculateRunnerState.test.js (4 tests) 5ms ✓ 基本前進計算 ✓ 含後退邏輯 ✓ 後退耗時計算 ✓ 無後退情況 ✓ calculateFinishTime.test.js (5 tests) 3ms ✓ 短距離計算 ✓ 觸發後退情況 ✓ 長距離計算 ✓ 無後退情況 ✓ 邊界條件 Test Files 2 passed (2) Tests 9 passed (9) Duration 278ms 1 2 3 4 5 6 7 ## 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