# Day 34：驗證器分層架構設計；While loop 時間序列狀態模擬

By [雞蛋糕的前端修煉屋](https://paragraph.com/@gcake) · 2026-01-08

---

**日期**：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**（斷言函式）

判斷真偽

`Boolean`

`isPrime(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: 輸入必須是質數
    

**三重身份**：

1.  **高階函式**：接受函式參數 + 返回函式
    
2.  **工廠函式**：批量製造客製化驗證器
    
3.  **包裝器模式**：增強 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` 重用 `useState`
    
*   Express 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 Pattern**

`predicateToValidator` 工廠函式

批量生成驗證器

**Wrapper Pattern**

Validator 包裝 Predicate

統一介面

**Strategy Pattern**

While 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**：重用已測試的模組
    
*   **分層**：底層工具 → 上層組合 → 業務邏輯
    

* * *

🔧 工具與技術棧
---------

*   **測試框架**：Vitest
    
*   **Node.js API**：`readline/promises`（Promise-based）
    
*   **程式碼風格**：Airbnb JavaScript Style Guide
    
*   **模組系統**：ES Modules (ESM)
    
*   **版本控制**：Git（小步提交）
    

* * *

📚 延伸學習資源
---------

### JavaScript 核心概念

*   [MDN - Closures（閉包）](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Closures)\[2\]
    
*   [MDN - import 動態載入](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Statements/import)\[2\]
    
*   [ECMAScript 規範 - Modulo 運算子](https://tc39.es/ecma262/)\[6\]
    

### 測試驅動開發

*   [TDD: Red-Green-Refactor](https://www.codecademy.com/article/tdd-red-green-refactor)\[5\]
    
*   [The TDD Cycle](https://dev.to/mungaben/the-tdd-cycle-red-green-refactor-1aaf)\[1\]
    

### 設計模式

*   [JavaScript Modulo Operator](https://www.freecodecamp.org/news/javascript-modulo-operator-how-to-use-the-modulus-in-js/)\[3\]
    
*   [Higher-Order Functions](https://dmitripavlutin.com/ecmascript-modules-nodejs/)\[7\]
    

* * *

💡 今日核心收穫
---------

1.  **Predicate vs Validator 分層**：底層判斷邏輯與上層驗證介面分離
    
2.  **工廠函式三重身份**：高階函式 + 工廠模式 + 包裝器
    
3.  **命名慣例**：`is*` 回傳布林，`ensure*` 回傳物件或拋錯
    
4.  **While Loop 思維**：可讀性優於效能（學習階段）
    
5.  **測試職責**：驗證工具正確性，不包含業務邏輯
    
6.  **模組重用**：下層工具被上層組合，上層工具被主程式使用
    
7.  **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](https://dev.to/mungaben/the-tdd-cycle-red-green-refactor-1aaf) [2](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Statements/import) [3](https://www.freecodecamp.org/news/javascript-modulo-operator-how-to-use-the-modulus-in-js/) [4](https://codedamn.com/news/javascript/modulo-operator) [5](https://www.codecademy.com/article/tdd-red-green-refactor) [6](https://tc39.es/ecma262/) [7](https://dmitripavlutin.com/ecmascript-modules-nodejs/)

---

*Originally published on [雞蛋糕的前端修煉屋](https://paragraph.com/@gcake/day-34)*
