2026年9月15日 星期二

TDD、DDD、SDD:從測試、領域設計到規格驅動開發,建立可維護的軟體工程流程

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):

PHP
// 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做出來的東西對不對?ImplementationTestsRegression / Incorrect Behavior
DDD我們應該如何建模?DomainDomain ModelBusiness Complexity
SDD我們到底要做什麼?RequirementSpecificationAmbiguous Requirements

簡化來說:

  • SDD 關注「我們到底要做什麼」
  • DDD 關注「我們應該如何理解與建模這個問題」
  • TDD 關注「我們如何確認實作真的符合預期」

但這不是絕對的線性規則。實務上它們會互相回饋,任何一環發現問題,都可能回到其他環節。

6. 三者如何組合?(Feedback Loop,而非瀑布)

實務流程大致如下:

text
需求
  ↓
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 的行為方法中。
  • 實作上的取捨:
PHP
// 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

PHP
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 行為方法。

PHP
// 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. 常見誤區

  1. TDD = 測試覆蓋率越高越好
    修正:關注行為覆蓋與回歸防護,而不是數字。Trivial 測試沒有價值。
  2. DDD = Repository Pattern
    修正:Repository 只是可能的工具。核心是 Domain Model 與業務規則表達。
  3. DDD = 每個 Model 都要 Entity
    修正:只有真正有身份與生命週期的概念才需要。很多東西就是 Data。
  4. Service 越多代表架構越好
    修正:Service 過多常常是缺少 Domain 行為或過度分層的徵兆。
  5. SDD = 寫更多文件
    修正:目標是可驗證、可驅動實作與測試的規格,而不是文件堆積。
  6. AI 寫測試就等於有 TDD
    修正:AI 產生的測試常常只測 happy path 或自己想像的行為。測試必須反映真正需求。
  7. 有規格就代表需求一定正確
    修正:規格本身也需要驗證與迭代。錯誤的規格會被高效實作出來。
  8. 測試通過就代表商業需求正確
    修正:測試只驗證「符合既有規格」。規格錯了,測試也會一起錯。
  9. 所有專案都應該使用完整 DDD
    修正:多數 CRUD 不值得。業務複雜度才是觸發條件。
  10. AI 產生的程式碼只要能跑就可以
    修正:能跑只是最低標準。還要符合業務規則、既有架構、可維護性與測試約束。

11. 最後提出一套適合現代後端工程師的工作方式

簡潔可落地的流程:

  1. Understand — 先理解問題與未知之處。AI 幫忙整理資訊、列出問題,但不能自行猜需求。
  2. Specify — 產出可驗證的 Acceptance Criteria、Business Rules、Edge Cases、API Contract。AI 協助轉寫與補全,工程師負責確認。
  3. Model — 決定 Domain 邊界與關鍵規則放哪裡。AI 可提出建議,工程師做最終決策。
  4. Test — 寫可執行的行為測試。AI 可產生測試骨架,但案例必須對齊規格。
  5. Implement — 最小必要修改。AI 寫實作,但範圍受規格與測試約束。
  6. Verify — 跑測試、看 diff、檢查 regression。AI 協助執行與初步分析。
  7. Refactor — 在測試保護下整理。AI 可協助,但不能自行擴大修改範圍。

AI 在每個階段都是加速器與建議者,而不是決策者。工程師對需求正確性、領域模型、行為驗證負最終責任。

12. 結論

真正成熟的工程流程,不是讓工程師寫更多程式碼,而是在寫程式碼之前,把問題、規則與驗證方式變得足夠清楚。

AI 讓 Coding 的成本下降,因此真正昂貴的反而變成 Requirement、Domain Understanding、Verification。TDD、DDD、SDD 不是讓你變得更「正統」,而是幫你在 AI 加速的時代,仍然能把「正確」與「可維護」守住。


TDD / DDD / SDD + AI 一頁式心智模型

text
┌─────────────────────────────────────────────────────────────┐
│                    Problem Space                             │
│  需求模糊 / 業務複雜 / 行為易回歸 / AI 容易亂猜               │
└──────────────────────────┬──────────────────────────────────┘
                           │
           ┌───────────────┼───────────────┐
           ▼               ▼               ▼
     ┌─────────┐     ┌─────────┐     ┌─────────┐
     │   SDD   │     │   DDD   │     │   TDD   │
     │ 要做什麼 │     │ 如何理解 │     │ 如何驗證 │
     │ Spec    │     │ Model   │     │ Tests   │
     └────┬────┘     └────┬────┘     └────┬────┘
          │               │               │
          └───────────────┼───────────────┘
                          ▼
                 Feedback Loop
          (規格 ↔ 模型 ↔ 測試 ↔ 實作)
                          │
                          ▼
              ┌───────────────────────┐
              │   AI as Accelerator   │
              │  - 受規格約束          │
              │  - 受模型語言約束      │
              │  - 受測試護欄約束      │
              │  - 工程師做最終決策    │
              └───────────────────────┘
                          │
                          ▼
              可維護、可驗證、可演化的系統

用這套思路,而不是追求完美 Pattern 數量,會更接近實務中真正能長期維護的後端系統。

沒有留言:

張貼留言

【2026 最新】VS Code 導入 pstack AI Skills 完整教學:用 Copilot 打造嚴謹的 Agent 工作流

在 VS Code 使用 AI Skills 打造嚴謹工程工作流:從 pstack 到原生 AGENTS.md 實戰指南 「If you want to go fast, go deep first.」 — Lauren Tan(@poteto) 進入 2026 年, AI Co...