線上課程觀課進度管理小工具開發日誌
Day 6:JavaScript 程式碼執行排序:遞迴函數、Call Stack、Task Queue
Call Stack;遞迴函數與 call stack 的關係;Task Queue(非同步的概念再釐清)
Day 9:「修煉圈圈 Practice Rings」Web app 開發日誌 - 2025 六角 AI Vibe Coding 體驗營期末大魔王作業
這份筆記旨在記錄 2025 六角 Vibe Coding 體驗營每日作業 Day 21 的期末專案成果,從一個簡單的個人痛點出發,透過與 AI(在我這個情境中是 Perplexity)協作,在一天內完成了一個包含前後端的全端網頁小工具:「修煉圈圈 Practice Rings」。
線上課程觀課進度管理小工具開發日誌
Day 6:JavaScript 程式碼執行排序:遞迴函數、Call Stack、Task Queue
Call Stack;遞迴函數與 call stack 的關係;Task Queue(非同步的概念再釐清)
Day 9:「修煉圈圈 Practice Rings」Web app 開發日誌 - 2025 六角 AI Vibe Coding 體驗營期末大魔王作業
這份筆記旨在記錄 2025 六角 Vibe Coding 體驗營每日作業 Day 21 的期末專案成果,從一個簡單的個人痛點出發,透過與 AI(在我這個情境中是 Perplexity)協作,在一天內完成了一個包含前後端的全端網頁小工具:「修煉圈圈 Practice Rings」。
<100 subscribers
<100 subscribers
日期:2026/01/08
主題:驗證器架構設計、While Loop 狀態模擬、測試職責分離
Predicate vs Validator 分層架構
工廠函式(Factory Function)實踐
命名慣例統一(is* vs ensure*)
While Loop 狀態模擬(時間序列計算)
模組重用模式
函數命名重構
Red-Green-Refactor 循環
測試與業務邏輯分離
Mock API 設計
層級 | 名稱 | 職責 | 回傳值 | 範例 |
|---|---|---|---|---|
底層 | Predicate(斷言函式) | 判斷真偽 |
|
|
上層 | Validator(驗證器) | 驗證 + 轉換 |
|
|
設計原則 :[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
每次調用都能存取這些封閉變數
前綴 | 語意 | 回傳值 | 使用情境 |
|---|---|---|---|
| 判斷/詢問 |
| 條件判斷、陣列過濾 |
| 確保/斷言 |
| 輸入驗證、資料轉換 |
| 型別轉換 | 轉換後的值 | 類型轉換 |
// ❌ 前:語意混淆
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]
情境:計算「競賽者在有週期性規則下到達終點的時間」
方案 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;
}
理由 :[1]
可讀性:程式碼像在說故事,逐秒模擬過程
可擴展性:規則複雜化時易於修改
學習價值:直接對應現實思維模式
除錯容易:可在迴圈內加 console.log 追蹤狀態
適用情境:
學習階段(理解優先於效能)
規則可能變動(需求不明確)
複雜邏輯(數學公式難以推導)
何時選數學公式:
效能關鍵(處理百萬級資料)
規則固定且簡單
生產環境優化階段
// ❌ 錯誤:在測試中寫業務邏輯
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} 秒`);
}
核心原則:
層級 | 職責 | 輸出方式 |
|---|---|---|
測試 | 驗證工具函數正確性 |
|
主程式 | 組合工具函數解決問題 |
|
/**
* 計算跑者在特定時間點的狀態(含週期性規則)
* @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]
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:路由處理器重用驗證中介層
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);
});
🔴 Red → 寫測試 → 測試失敗(預期行為未實作) 🟢 Green → 實作功能 → 測試通過 🔄 Refactor → 優化程式碼 → 測試持續通過
// 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 參數錯誤');
});
});
執行測試: 失敗(功能未實作)
// 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 };
}
執行測試: 通過
// 抽取驗證邏輯
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 };
}
執行測試: 持續通過
模式 | 應用 | 效益 |
|---|---|---|
Factory Pattern |
| 批量生成驗證器 |
Wrapper Pattern | Validator 包裝 Predicate | 統一介面 |
Strategy Pattern | While Loop vs 數學公式 | 彈性切換演算法 |
Module Pattern | 分層模組設計 | 可重用、可測試 |
// ❌ 不好的命名
calculator(20, 5, 5, 1) // 名詞,不清楚動作
isPositive(x) // 回傳 { value } // 語意不符
// ✅ 好的命名
calculateRunnerState(20, 5, 5, 1) // 動詞+名詞,清楚動作
ensurePositive(x) // 回傳 { value } // 語意一致
isPositive(x) // 回傳 Boolean // Predicate 用法
核心優勢:
每次改動小 → 錯誤容易定位
持續通過測試 → 信心累積
可隨時重構 → 測試保護網
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(小步提交)
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
日期:2026/01/08
主題:驗證器架構設計、While Loop 狀態模擬、測試職責分離
Predicate vs Validator 分層架構
工廠函式(Factory Function)實踐
命名慣例統一(is* vs ensure*)
While Loop 狀態模擬(時間序列計算)
模組重用模式
函數命名重構
Red-Green-Refactor 循環
測試與業務邏輯分離
Mock API 設計
層級 | 名稱 | 職責 | 回傳值 | 範例 |
|---|---|---|---|---|
底層 | Predicate(斷言函式) | 判斷真偽 |
|
|
上層 | Validator(驗證器) | 驗證 + 轉換 |
|
|
設計原則 :[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
每次調用都能存取這些封閉變數
前綴 | 語意 | 回傳值 | 使用情境 |
|---|---|---|---|
| 判斷/詢問 |
| 條件判斷、陣列過濾 |
| 確保/斷言 |
| 輸入驗證、資料轉換 |
| 型別轉換 | 轉換後的值 | 類型轉換 |
// ❌ 前:語意混淆
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]
情境:計算「競賽者在有週期性規則下到達終點的時間」
方案 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;
}
理由 :[1]
可讀性:程式碼像在說故事,逐秒模擬過程
可擴展性:規則複雜化時易於修改
學習價值:直接對應現實思維模式
除錯容易:可在迴圈內加 console.log 追蹤狀態
適用情境:
學習階段(理解優先於效能)
規則可能變動(需求不明確)
複雜邏輯(數學公式難以推導)
何時選數學公式:
效能關鍵(處理百萬級資料)
規則固定且簡單
生產環境優化階段
// ❌ 錯誤:在測試中寫業務邏輯
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} 秒`);
}
核心原則:
層級 | 職責 | 輸出方式 |
|---|---|---|
測試 | 驗證工具函數正確性 |
|
主程式 | 組合工具函數解決問題 |
|
/**
* 計算跑者在特定時間點的狀態(含週期性規則)
* @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]
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:路由處理器重用驗證中介層
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);
});
🔴 Red → 寫測試 → 測試失敗(預期行為未實作) 🟢 Green → 實作功能 → 測試通過 🔄 Refactor → 優化程式碼 → 測試持續通過
// 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 參數錯誤');
});
});
執行測試: 失敗(功能未實作)
// 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 };
}
執行測試: 通過
// 抽取驗證邏輯
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 };
}
執行測試: 持續通過
模式 | 應用 | 效益 |
|---|---|---|
Factory Pattern |
| 批量生成驗證器 |
Wrapper Pattern | Validator 包裝 Predicate | 統一介面 |
Strategy Pattern | While Loop vs 數學公式 | 彈性切換演算法 |
Module Pattern | 分層模組設計 | 可重用、可測試 |
// ❌ 不好的命名
calculator(20, 5, 5, 1) // 名詞,不清楚動作
isPositive(x) // 回傳 { value } // 語意不符
// ✅ 好的命名
calculateRunnerState(20, 5, 5, 1) // 動詞+名詞,清楚動作
ensurePositive(x) // 回傳 { value } // 語意一致
isPositive(x) // 回傳 Boolean // Predicate 用法
核心優勢:
每次改動小 → 錯誤容易定位
持續通過測試 → 信心累積
可隨時重構 → 測試保護網
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(小步提交)
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
Share Dialog
Share Dialog
No comments yet