TDD、DDD、SDD:從測試、領域設計到規格驅動開發,建立可維護的軟體工程流程
1. 開場:為什麼現在又要談 TDD、DDD、SDD?
AI 現在能快速產出大量程式碼。一個 CRUD API、一個 Service、甚至一整套 Feature Test,幾分鐘就能生成。問題是:這些程式碼「能跑」與「正確」是兩回事。
專案越大,修改一個功能就越容易牽動其他地方。工程師常常直接進入 coding,卻沒有先釐清問題邊界。需求、規格、測試、程式碼之間容易脫節——有人腦中的業務規則、有 Jira 上的模糊描述、有 AI 自己猜出來的欄位與 abstraction、有通過但沒有真正驗證業務行為的測試。
結果就是:表面看起來有測試覆蓋、有架構分層,實際上改一個權限規則就要翻半個系統,或者 AI 產出的程式「看起來合理」卻違反既有業務約束。
TDD、DDD、SDD 並不是三個互相競爭的方法,也不是什麼銀彈。它們分別處理不同層次的問題:
- SDD 回答:「我們到底要做什麼?」
- DDD 回答:「我們應該如何理解與設計這個問題?」
- TDD 回答:「我們如何確認實作真的符合預期?」
這不是三種嚴格按照先後順序執行的方法論,而是三個不同關注面的工程實踐。實務上它們會以 feedback loop 的方式互相影響,而不是瀑布式 pipeline。
AI Coding 時代讓這條鏈變得更關鍵,因為 coding 的成本大幅下降,真正昂貴的變成了「釐清問題、建模、驗證」。
2. TDD:先建立「可驗證性」
Test-Driven Development 的核心循環是 Red → Green → Refactor。它真正解決的不是「提高 coverage 數字」,而是建立可執行的行為規格。測試是在描述「系統應該如何回應」,而不是事後補上的檢查清單。
以 Laravel 實務為例。需求:「建立一個 API,只有當使用者具有某個權限時才能刪除資料。」
以下權限相關範例假設專案使用 Spatie Laravel Permission 或相容的 Permission Model。Laravel 原生並沒有 givePermissionTo() 方法。若使用其他權限套件,請替換成對應 API。
先寫測試(Feature / API Test):
// tests/Feature/EnterpriseDeleteTest.php
public function test_user_with_delete_permission_can_delete_enterprise(): void
{
$user = User::factory()->create();
$user->givePermissionTo('enterprise.delete');
$enterprise = Enterprise::factory()->create();
$response = $this->actingAs($user)
->deleteJson("/api/enterprises/{$enterprise->id}");
$response->assertNoContent();
$this->assertDatabaseMissing('enterprises', ['id' => $enterprise->id]);
}
public function test_user_without_delete_permission_cannot_delete_enterprise(): void
{
$user = User::factory()->create(); // 沒有 permission
$enterprise = Enterprise::factory()->create();
$response = $this->actingAs($user)
->deleteJson("/api/enterprises/{$enterprise->id}");
$response->assertForbidden();
$this->assertDatabaseHas('enterprises', ['id' => $enterprise->id]);
}先跑測試 → 失敗(Red)。再寫最小實作讓它通過(Green),然後再重構(Refactor)。不要一開始就寫完整 Service、DTO、Exception 階層。
在企業 Laravel 專案中,測試分工大致如下:
- Unit Test:純邏輯、Value Object、Domain Service(幾乎不碰 DB、HTTP)。
- Feature / API Test:從 HTTP 入口驗證完整行為與權限,在 Laravel 後端專案中特別適合作為可執行規格的一部分。
- Integration Test:涉及多個外部系統或複雜 transaction 時使用。
實際上 Unit / Feature / Integration 的邊界會依專案而異。重點不是名稱,而是「這組測試是否能有效保護我們真正在意的行為」。不要把所有東西都塞進 Unit Test,也不要為了 coverage 而測 trivial getter。
TDD 的價值在於:之後任何人(包括 AI)改程式時,都有一組可執行的行為約束。測試失敗就代表行為被改變了。
3. DDD:先搞清楚「我們到底在解決什麼問題」
Domain-Driven Design 最常被誤解的地方,是把它等同於「把 Model 改成 Entity + 建一堆 Repository + Service」。那不是 DDD,那是 Pattern 堆砌。
DDD 的核心是:讓程式模型反映業務領域與業務規則。Ubiquitous Language 要在團隊與程式碼中一致。Bounded Context 幫你劃分「哪些東西屬於同一個問題空間」。
拿企業官網後台的「投資人/公司/分類/多語系內容管理」當例子。
真正的 Domain Rule 可能是:
- 一個公司在某個語系下只能有一個「主要投資人」描述。
- 刪除分類時,如果底下還有已發布內容,必須先處理關聯或禁止刪除。
- 多語系內容的「發布狀態」是跨語系協調的,而不是單純每個翻譯獨立。
這些規則值得放進 Domain Model(Entity、Value Object、Aggregate 的行為方法,或 Domain Service)。反過來說,單純的 CRUD 欄位編輯、列表篩選、圖片上傳,通常不需要完整 Aggregate 或 Repository 抽象。
Laravel 實務建議:
- Eloquent Model 可以直接承載部分 Domain 行為(如果規則不複雜)。
- 真正複雜的規則再抽到 Domain Service 或 Application Service。
- Repository 只有在你需要真正隔離 persistence 細節、或有多種儲存策略時才值得。
- 不是每個 Model 都要變成 Entity,也不是每個操作都要 Application Service。
「不是所有 CRUD 都值得 DDD 化。」這句話要反覆提醒自己。DDD 的成本在於認知負擔與程式碼結構複雜度。只有當業務複雜度高到「直接寫在 Controller / Model 會很快失控」時,才值得投入。
4. SDD:規格驅動開發
本文所說的 SDD,是指 Specification-Driven Development / Spec-Driven Development。
需要先說明:SDD 並不像 TDD 或 DDD 那樣有高度標準化、單一且被廣泛接受的業界定義。不同團隊可能把 SDD 理解成不同流程與產物。因此本文明確定義為:
先建立明確、可驗證、可供人與 AI 共同理解的規格,再進入實作。
它通常包含:
- Functional Requirement
- Acceptance Criteria
- Business Rules
- API Contract / Data Contract
- Permission Rules
- Edge Cases
- Error Handling
- 對應的 Test Cases
SDD 的價值不是「多寫文件」,而是降低這條鏈上的資訊損失:
人腦中的需求 → AI / 工程師理解 → 程式碼
沒有明確規格時,AI 最容易開始「猜」:自己加欄位、自己加 Service、自己決定錯誤碼、自己假設權限模型。工程師也容易直接開始寫,寫到一半才發現需求沒講清楚。
規格要可驗證。最好能直接對應到測試案例或 Acceptance Criteria。模糊的「要支援刪除」不如「只有具備 enterprise.delete 權限的使用者可以刪除;若有關聯資料則禁止刪除並回傳特定錯誤」。
5. TDD、DDD、SDD 三者到底有什麼差別?
| 方法 | 核心問題 | 關注層次 | 主要產物 | 解決的風險 |
|---|---|---|---|---|
| TDD | 做出來的東西對不對? | Implementation | Tests | Regression / Incorrect Behavior |
| DDD | 我們應該如何建模? | Domain | Domain Model | Business Complexity |
| SDD | 我們到底要做什麼? | Requirement | Specification | Ambiguous Requirements |
簡化來說:
- SDD 關注「我們到底要做什麼」
- DDD 關注「我們應該如何理解與建模這個問題」
- TDD 關注「我們如何確認實作真的符合預期」
但這不是絕對的線性規則。實務上它們會互相回饋,任何一環發現問題,都可能回到其他環節。
6. 三者如何組合?(Feedback Loop,而非瀑布)
實務流程大致如下:
需求
↓
SDD(釐清 Business Rules / Acceptance Criteria / Edge Cases)
↓
DDD(建立 Domain Model,決定哪些規則放哪裡)
↓
TDD(寫可驗證的行為測試)
↓
Implementation(最小必要實作)
↓
Refactor
↓
Integration / Acceptance Test實際專案幾乎從來不是完美線性:
- TDD 寫到一半發現需求有洞 → 回到 SDD
- 實作時發現 Domain Model 不合理 → 回到 DDD
- DDD 過程中發現業務規則根本沒講清楚 → 回到 SDD
- 測試發現規格與實際業務矛盾 → 回到 Requirement
真正的流程是 feedback loop,而不是瀑布。文件再漂亮,如果無法驅動測試與實作,價值有限。
7. AI Coding 時代,為什麼 SDD + DDD + TDD 特別重要?
這是全文重點。
AI 很擅長:
- 產生 CRUD
- 補程式碼
- 依照既有 Pattern 重構
- 寫測試骨架
- 根據既有程式修改
但 AI 不應該被當成「知道需求的 Senior Engineer」。它最容易犯的錯誤包括:
- 猜需求、自己增加欄位或行為
- 引入不必要的 abstraction(多一層 Service、DTO、Repository)
- 忽略既有架構與邊界
- 只修表面問題
- 寫出可以執行但不符合業務規則的程式
AI 並不是不會寫程式,而是可以很有效率地實作「錯誤的需求」。
因此:
- SDD 約束 AI 要做什麼:明確的 Acceptance Criteria、API Contract、Permission Rule、Edge Cases。AI 不該自由發揮。
- DDD 約束 AI 應該如何理解業務:Domain Model、Ubiquitous Language、Bounded Context。AI 提出的建議要對齊既有領域語言。
- TDD 約束 AI 不能隨便改變既有行為:測試是行為的護欄。AI 改完程式後必須通過既有測試,新增行為也要有對應測試。
AI + TDD:讓 AI 產生或補齊測試,但測試案例必須反映真正需求,而不是 AI 自己想像的 happy path。
AI + DDD:AI 可以提出 Domain Model 建議,但最終建模決策由工程師負責,避免 AI 為了「看起來專業」而過度抽象。
AI + SDD:最關鍵。沒有規格時,AI 產出的程式碼品質上限很低。有明確規格時,AI 變成強大的實作加速器。
AI 降低了 coding cost,卻沒有降低 requirement clarification、domain understanding 和 verification 的成本。這才是現代後端工程真正昂貴的地方。
8. 一個完整 Laravel 實戰案例
需求:「後台使用者可以刪除某個企業資料,但只有具備對應 Delete Permission 的使用者才能刪除;如果資料具有關聯資料,必須依照業務規則處理。」
SDD
- Functional Requirement:提供 DELETE /api/enterprises/{id}。
- Permission Rule:使用者必須具備 enterprise.delete 權限。
- Business Rule:若該企業下仍有已發布的關聯內容(例如投資人報告),禁止刪除,並回傳 422 與明確業務錯誤訊息。
- Acceptance Criteria:
- 有權限且無阻礙關聯 → 204 No Content,資料被刪除。
- 無權限 → 403 Forbidden。
- 資料不存在 → 404 Not Found。
- 有阻礙關聯 → 422 Unprocessable Entity,附帶明確訊息。
- Edge Cases:關聯內容已 soft-deleted、權限中途被撤銷、並發刪除請求等。
DDD
- Entity:Enterprise(可承載部分業務行為)。
- Domain Rule:刪除前是否允許,屬於業務規則。實務上可放在 Model 的行為方法中。
- 實作上的取捨:
// app/Models/Enterprise.php
public function canBeDeleted(): bool
{
// 這是 pragmatic 的做法:把「查詢關聯狀態」與「業務判斷」放在一起。
// 若未來規則變得更複雜(跨 Bounded Context、需要多種資料來源),
// 再考慮抽出 Domain Service 或把條件透過參數注入,以減少對 Eloquent 的直接依賴。
return !$this->publishedReports()->exists();
}- Application Layer:協調權限檢查 + Domain 行為 + Persistence。
- Permission Boundary:權限檢查放在 Policy,不要塞進 Domain Entity。
- 不需要的東西:不需要為了這個功能再建完整 Repository 介面、Command Bus、一堆 DTO,除非既有架構已經這樣做。
重點再次強調:
「能用現有 Laravel Pattern(Policy + Model 行為方法)清楚表達規則,就不要為了追求純正 DDD 而額外引入抽象。」
TDD
public function test_authorized_user_can_delete_enterprise_without_blocking_relations(): void { /* ... */ }
public function test_unauthorized_user_gets_forbidden(): void { /* ... */ }
public function test_non_existent_enterprise_returns_404(): void { /* ... */ }
public function test_enterprise_with_blocking_relations_cannot_be_deleted(): void { /* ... */ }Implementation(保持 KISS)
使用 Laravel 常見實務:Policy + Controller + Eloquent 行為方法。
// app/Policies/EnterprisePolicy.php
public function delete(User $user, Enterprise $enterprise): bool
{
return $user->can('enterprise.delete');
}
// Controller
public function destroy(Enterprise $enterprise)
{
$this->authorize('delete', $enterprise);
if (!$enterprise->canBeDeleted()) {
return response()->json([
'message' => 'Cannot delete enterprise with published related reports.',
], 422);
}
$enterprise->delete();
return response()->noContent();
}對應的錯誤回應契約建議保持一致:
- 403:無權限
- 404:資源不存在
- 422:違反業務規則(有阻礙關聯)
為什麼這樣設計?因為這個功能的複雜度還沒到需要完整 Aggregate Root + Domain Service + Application Service 的程度。能用 Policy + Model 行為解決,就不要為了 Pattern 而 Pattern。如果之後規則變得非常複雜(多種刪除策略、事件溯源、跨 Bounded Context),再演化。
9. 什麼情況不需要 TDD / DDD / SDD?
- 非常簡單的 CRUD、內部一次性 script、原型驗證:不需要完整 DDD,也不一定值得建立完整測試架構。
- 純 UI 調整、沒有複雜業務規則的前端頁面:完整 SDD 可能過重。
- 探索性程式或 throw-away code:過度規格與測試是浪費。
但對以下情況,應該提高規格、建模與測試的要求:
- 金流、權限、訂單、多租戶
- 複雜工作流程、高風險資料操作
- 複雜業務規則
- AI Agent 會執行的重要操作
原則是:風險與複雜度越高,SDD + DDD + TDD 的投入越值得。
10. 常見誤區
- TDD = 測試覆蓋率越高越好
修正:關注行為覆蓋與回歸防護,而不是數字。Trivial 測試沒有價值。 - DDD = Repository Pattern
修正:Repository 只是可能的工具。核心是 Domain Model 與業務規則表達。 - DDD = 每個 Model 都要 Entity
修正:只有真正有身份與生命週期的概念才需要。很多東西就是 Data。 - Service 越多代表架構越好
修正:Service 過多常常是缺少 Domain 行為或過度分層的徵兆。 - SDD = 寫更多文件
修正:目標是可驗證、可驅動實作與測試的規格,而不是文件堆積。 - AI 寫測試就等於有 TDD
修正:AI 產生的測試常常只測 happy path 或自己想像的行為。測試必須反映真正需求。 - 有規格就代表需求一定正確
修正:規格本身也需要驗證與迭代。錯誤的規格會被高效實作出來。 - 測試通過就代表商業需求正確
修正:測試只驗證「符合既有規格」。規格錯了,測試也會一起錯。 - 所有專案都應該使用完整 DDD
修正:多數 CRUD 不值得。業務複雜度才是觸發條件。 - AI 產生的程式碼只要能跑就可以
修正:能跑只是最低標準。還要符合業務規則、既有架構、可維護性與測試約束。
11. 最後提出一套適合現代後端工程師的工作方式
簡潔可落地的流程:
- Understand — 先理解問題與未知之處。AI 幫忙整理資訊、列出問題,但不能自行猜需求。
- Specify — 產出可驗證的 Acceptance Criteria、Business Rules、Edge Cases、API Contract。AI 協助轉寫與補全,工程師負責確認。
- Model — 決定 Domain 邊界與關鍵規則放哪裡。AI 可提出建議,工程師做最終決策。
- Test — 寫可執行的行為測試。AI 可產生測試骨架,但案例必須對齊規格。
- Implement — 最小必要修改。AI 寫實作,但範圍受規格與測試約束。
- Verify — 跑測試、看 diff、檢查 regression。AI 協助執行與初步分析。
- Refactor — 在測試保護下整理。AI 可協助,但不能自行擴大修改範圍。
AI 在每個階段都是加速器與建議者,而不是決策者。工程師對需求正確性、領域模型、行為驗證負最終責任。
12. 結論
真正成熟的工程流程,不是讓工程師寫更多程式碼,而是在寫程式碼之前,把問題、規則與驗證方式變得足夠清楚。
AI 讓 Coding 的成本下降,因此真正昂貴的反而變成 Requirement、Domain Understanding、Verification。TDD、DDD、SDD 不是讓你變得更「正統」,而是幫你在 AI 加速的時代,仍然能把「正確」與「可維護」守住。
TDD / DDD / SDD + AI 一頁式心智模型
┌─────────────────────────────────────────────────────────────┐
│ Problem Space │
│ 需求模糊 / 業務複雜 / 行為易回歸 / AI 容易亂猜 │
└──────────────────────────┬──────────────────────────────────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ SDD │ │ DDD │ │ TDD │
│ 要做什麼 │ │ 如何理解 │ │ 如何驗證 │
│ Spec │ │ Model │ │ Tests │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
└───────────────┼───────────────┘
▼
Feedback Loop
(規格 ↔ 模型 ↔ 測試 ↔ 實作)
│
▼
┌───────────────────────┐
│ AI as Accelerator │
│ - 受規格約束 │
│ - 受模型語言約束 │
│ - 受測試護欄約束 │
│ - 工程師做最終決策 │
└───────────────────────┘
│
▼
可維護、可驗證、可演化的系統用這套思路,而不是追求完美 Pattern 數量,會更接近實務中真正能長期維護的後端系統。
沒有留言:
張貼留言