看到「新聞發佈系統」這幾個字,你腦中浮現的畫面可能是:一個 Article 資料表、幾個 CRUD API,隨便一個資淺工程師半天就能生出來,架構好壞根本不重要。
這句話的前半段沒錯——這個示範系統的業務規則真的只有 4 條:標題不可為空、已發佈文章不可重複發佈、已發佈文章需先下架才能編輯、其餘是基本的建立/查詢/刪除。但正因為業務邏輯這麼簡單,它才適合拿來做對照實驗:如果連這麼簡單的需求都能被寫成 21,727 行,那複雜度的來源顯然不是業務本身,而是寫程式的人(或 AI)自己加上去的。
今天先看乾淨版本長什麼樣子——用 Outside-In TDD/ATDD 寫出來、只有 3 個檔案、249 行的版本 A。
Outside-In TDD/ATDD 的核心精神,是先用貼近使用者語言的驗收測試描述「系統應該做什麼」,再逐步往內寫出讓測試通過所需要的實作。這個示範專案的驗收測試用 Given-When-Then 的敘事方式寫成,例如其中一條:
it('should mark article as published when publishing a draft article', function () {
// Given:建立一篇草稿文章
$id = $this->service->create('標題', '內容');
// When:發佈這篇文章
$this->service->publish($id);
// Then:狀態應變成已發佈,且有發佈時間
$article = $this->repository->find($id);
expect($article->status())->toBe('published')
->and($article->publishedAt())->toBeInstanceOf(DateTimeImmutable::class);
});
全部一共 10 個這樣的測試,涵蓋建立、發佈、重複發佈要噴例外、下架、編輯、已發佈不可編輯、列表查詢(公開列表只顯示已發佈、後台列表顯示全部)、刪除。這 10 個測試,就是版本 A 跟版本 B 唯一共同的、一字不改的規格。
Article.php——業務規則長在物件裡,不是散落各處public function publish(DateTimeImmutable $publishedAt): void
{
if ($this->status === 'published') {
throw new \RuntimeException('文章已經發佈過,不可重複發佈');
}
$this->status = 'published';
$this->publishedAt = $publishedAt;
}
public function edit(string $title, string $content): void
{
if ($this->status !== 'draft') {
throw new \RuntimeException('已發佈文章需先下架才能編輯');
}
if ($title === '') {
throw new \InvalidArgumentException('標題不可為空');
}
$this->title = $title;
$this->content = $content;
}
「已發佈不可重複發佈」「已發佈需先下架才能編輯」這兩條規則,各自只出現在一個地方:對應的方法本身。沒有 Specification、沒有 Validator、沒有另外一層規則物件——因為驅動這個類別長出來的,是外層驗收測試的斷言,而不是「這種情境理論上應該要有一個規則物件」的預先假設。
ArticleRepository.php——資料存取的唯一入口一個類別直接包 PDO,save()/find()/findPublished()/findAll()/delete() 五個方法,SQL 寫在方法裡,沒有額外的 QueryBuilder、TableGateway、Mapper。
PublishArticleService.php——應用服務,直接對應每一個使用情境public function publish(int $id): void
{
$article = $this->findOrFail($id);
$article->publish(new DateTimeImmutable());
$this->repository->save($article);
}
每個方法都是「找出來 → 呼叫業務方法 → 存回去」三步驟,沒有 Command、沒有 CommandBus、沒有 Handler。呼叫鏈就是:測試 → PublishArticleService → Article / ArticleRepository,兩層,結束。
❌ 常見的「先把架構搭好」直覺:
先加 Repository 介面 + 實作,
再加 Service 介面,
再加 DTO 避免曝露 Domain 物件,
再加 Command/Handler 為了「未來的擴充性」……
✅ Outside-In TDD 的實際順序:
寫一條 Given-When-Then 測試 → 跑紅燈
→ 寫出讓它變綠燈的最小程式碼
→ 重構
→ 下一條測試
3 個檔案、249 行,不是因為這個團隊偷懶,而是因為沒有一行程式碼是測試沒有要求就先寫出來的。
如果你只看「業務規則只有 4 條」這件事,可能會覺得版本 A 的簡單是「本來就該這麣簡單」,沒什麼了不起。但 Day 10 會讓你看到,同樣的 4 條規則、同樣的 10 個測試,可以被寫成 745 個檔案、21,727 行——業務複雜度是常數,程式碼複雜度卻可以是變數。**這正是這個系列反覆強調的主題句的具體證據:AI 沒有發明過度設計,它只是讓過度設計的速度追上了你按下 Enter 的速度;Review 要跟得上,審的就不能再是程式碼本身,而是產生程式碼的規則。**先建立版本 A 這個基準線,Day 10 之後的每一次對照才有意義。
如果你今天要求 AI 幫你實作這 10 條驗收測試,你猜它會不會自動長出跟版本 A 一樣精簡的 3 個檔案?如果不會,你覺得差異會出現在哪一步?
Article、ArticleRepository、PublishArticleService 3 個檔案、249 行Day 10 會展示同一組測試、AI 自由發揮寫出來的版本 B——745 個檔案、21,727 行,包含 CQRS、四層 Repository 裝飾器、事件系統的完整樣貌。
我第一次把版本 A 的 3 個檔案完整看過一遍時,其實有點心虛——這樣真的可以嗎?沒有 Repository 介面、沒有分層架構圖,會不會被說「不夠專業」?但仔細想想,這個系統目前的需求範圍,就只需要這樣。 我以前也曾經在需求還沒到之前,先把架構「鋪好」,結果鋪出來的東西大多數從來沒被用上。這次刻意做這個對照實驗,某種程度上是想親手驗證一次:如果紀律夠嚴謹,「剛好夠用」跟「專業」根本不衝突。