Day 14:JavaScript - TDD 與模組化重構(續)

這兩天花了不少時間在練習「輸入驗證」、「CLI 互動」與「核心邏輯」的責任分層上,並持續透過 Test-Driven Development (TDD) 的流程來實作演算法。這篇文章整理了我今天在實作過程中的一些設計決策與心得,特別是如何利用 Vitest 進行參數化測試,以及從命令式程式碼走向聲明式重構的過程。


一、三層責任分工:Parse, don’t validate

在處理使用者輸入時,很容易把「檢查邏輯」和「運算邏輯」混在一起。為了讓程式碼更乾淨,我嘗試將系統切分為三層,讓每一層只專注做一件事。

1. 驗證層 (Validation Layer)

這一層的責任是「守門員」:接收外部傳來的髒資料(例如使用者輸入的字串),負責清洗、轉型,最後只吐出「合法的原始型別值」或直接丟出錯誤。

例如,我們常需要檢查一個輸入是否為「正整數」。與其在業務邏輯裡到處寫 if (val < 0),不如抽成一個專用的 parsePositiveInt 函式。這裡我用到了一個好用的技巧:預設參數 (Default Parameters)。

  • 可以設計成 parsePositiveInt(input, min = 1)。

  • 預設只檢查是否 >= 1。

  • 如果某些情境需要更嚴格(例如大於 5),也可以彈性傳入 min 參數。

相關 MDN 參考:

2. CLI 互動層 (CLI Layer)

這一層負責跟使用者「對話」。它的工作很單純:問問題、拿答案、交給驗證層檢查。

  • 若驗證通過:就把乾淨的值往後傳。

  • 若驗證失敗(丟錯):直接 catch 起來印出錯誤訊息,然後再問一次。

這樣的設計讓 CLI 層完全不需要知道「什麼是合法的整數」,它只負責「重試」這個行為流程。

3. 核心邏輯層 (Core Logic Layer)

這是最乾淨的一層。它假設收到的參數一定是合法的(因為髒資料早在驗證層就被擋掉了)。

這樣做的好處是:核心函式裡不再充滿防禦性的 if-else,可以專心處理演算法或業務規則。這也讓這一層特別容易進行單元測試。


二、TDD 實戰:從寫死到演算法

在實作演算法(例如「判斷質數」或「陣列搜尋」)時,我採用了 TDD (Test-Driven Development) 的節奏:紅 (Red) → 綠 (Green) → 重構 (Refactor)。目前還在熟悉 Uncle Bob 提倡的「最小化測試與重構」這個每次只推進幾行程式碼的思考流程。

1. 善用參數化測試 (Parameterized Tests)

與其寫十幾個 it(...) 來測不同的數字,不如使用 Table-Driven 的方式把測資列成表格。在 Vitest 中可以使用 test.each:

describe('質數索引搜尋', () => {
test.each([
// [輸入陣列, 預期結果]
[, [{ val: 3, idx: 1 }, { val: 13, idx: 3 }]],​
[, []], // 沒找到的情況​
[[], []], // 空陣列邊界測試
])('輸入 %j,應回傳 %j', (input, expected) => {
expect(findPrimes(input)).toEqual(expected);
});
});

這種寫法讓測試與資料分離,新增案例時只需要在陣列裡多加一行,非常直觀。

Vitest 參考:Test API

2. 實作演化的三部曲

以「找出陣列中所有質數與位置」這個需求為例,我的 TDD 過程是這樣的:

  1. 第一版(寫死): 為了先確認測試架構沒問題,我會先直接 return 一個固定的陣列讓測試通過。這聽起來很蠢,但能確保「測試程式本身」是對的。

  2. 第二版(命令式:For Loop + Push): 這是最直覺的寫法。建立一個空陣列,跑迴圈,遇到符合條件的就 push 進去。 這時候的重點是「把邏輯寫對」,通過所有測試案例。

  3. 第三版(聲明式:Map + Filter): 在測試「保護」下,我可以放心地重構程式碼,把 for 迴圈改成更具語意的高階函式 (Higher-Order Function)。


三、從命令式到聲明式:HOF 的重構

在前端開發(尤其是 React/Vue)中,我們非常依賴 map、filter 這類高階函式 (HOF)。這次練習中,我也嘗試把原本的 for 迴圈重構成資料流管道 (Pipeline)。

什麼是 HOF (Higher-Order Function)?

簡單來說,只要一個函式能接收函式當參數,或回傳一個函式,它就是 HOF。

  • 命令式寫法 (Imperative): 「建立空陣列 → 跑迴圈 → 判斷條件 → push 進陣列」。這是在告訴電腦怎麼做 (How)。

  • 聲明式寫法 (Declarative): 使用 filter 篩選、用 map 轉換。這是在告訴電腦我要什麼 (What)。

MDN 參考:

透過 TDD 的保護,我可以確認重構前後的行為完全一致,但程式碼的可讀性與維護性卻得到了提升。


四、小結

這兩天的練習讓我體會到,寫程式不僅僅是把功能做出來,更重要的是架構的整潔度與測試的保護網。

  • 責任分層讓每一段程式碼都好測、好改。

  • TDD 讓我敢於重構,不怕改壞舊功能。

  • 參數化測試 讓邊界案例的測試變得輕鬆且完整。

從小型的練習題開始,每次都把整個架構流程完整跑過一遍,建立好的開發習慣。在實際的專案開發中,也是維持程式碼品質的重要基石。