iT邦幫忙

2026 iThome 鐵人賽

DAY 17
0
Software Development

當 AI 寫得比你讀得快:Code Review 該審什麼系列 第 17 篇

Day 17:Outside-In TDD/ATDD——最古老的過度設計解藥

  • 分享至 

  • xImage
  •  

前言:「這系列講了 16 天問題,到底什麼時候要講解法?」

如果你從 Day 1 一路看到這裡,大概已經受夠「過度設計有多可怕」這件事了——745 個檔案、21,727 行、92% 死碼,這些數字已經重複太多次。你現在心裡的問題應該很直接:那到底該怎麼避免?

我猜很多人期待的答案是某個新工具、新流程、或者一份「AI 使用規範 checklist」。但我要先破題:這個系列案例裡那個 249 行、3 個檔案的乾淨版本,靠的不是任何新東西,而是一套十幾年前就存在的紀律——Outside-In TDD/ATDD(Acceptance Test Driven Development)。它之所以能防住過度設計,不是因為它「教你少寫程式碼」,而是因為它改變了「一個類別為什麼會被建立」這件事的因果順序。

今日目標

  • 理解 Outside-In TDD/ATDD 的基本流程:先寫驗收測試,再由外而內逼出實作
  • 弄清楚「測試驅動類別誕生」跟「先設計架構再補測試」這兩種順序,為什麼會導出完全不同的程式碼量
  • 看懂 Day 9 案例裡那 3 個檔案,為什麼每一行都對應著某條驗收測試的具體斷言
  • 認識這套紀律為什麼比任何「事後 code review checklist」更早、更根本地擋住過度設計
  • 建立一個判斷習慣:看到一個類別,先問「它是被哪一條驗收測試逼出來的」

Outside-In TDD/ATDD 是什麼:由外而內、測試先行

Outside-In TDD 的流程很簡單:先從系統的最外層(使用者看得到的行為)寫一個 Given-When-Then 格式的驗收測試,這個測試一開始一定是紅的,因為連被測試呼叫的類別都還不存在。接著你只寫「剛好足夠讓這個測試變綠」的程式碼——不多寫一行。等到這條測試綠了,才進到下一條驗收測試,重複同樣的循環。

ATDD 是把這套流程放在「驗收測試」這個層級上的具體實踐:測試案例直接對應業務規則,而不是對應某個類別的內部方法。我們這個系列的示範案例 ai-news-test(main branch),驗收測試長這樣:

it('should throw exception when publishing an already published article', function () {
    $id = $this->service->create('標題', '內容');
    $this->service->publish($id);

    $this->service->publish($id);
})->throws(RuntimeException::class);

這條測試直接描述業務規則「已發佈的文章不能重複發佈」,沒有提到任何 Repository、Specification、Event 這類實作細節。10 條這樣的測試,最終逼出來的實作是 Article、ArticleRepository、PublishArticleService 這 3 個檔案、249 行——因為驗收測試從來沒有要求過第 4 個檔案。

為什麼這件事能擋住過度設計

這裡是整篇文章最關鍵的因果關係:在 Outside-In TDD 的紀律下,一個類別只有兩種存在理由——它被某條驗收測試直接逼出來,或者它是為了讓某個已存在的類別保持乾淨而做的內部重構產物。 沒有第三種「因為我覺得將來可能需要」的存在理由。

對照過度設計版本,SqliteUnitOfWork、RetryingArticleRepositoryDecorator 這類類別完全不是任何一條驗收測試逼出來的——沒有一條驗收測試斷言過「交易失敗要 rollback」或「網路失敗要重試」。它們是工程師(或 AI)在下筆時,憑著「專業直覺」預先建好的。這正是這個系列反覆講的主題句在這裡的具體體現:AI 沒有發明過度設計,它只是讓過度設計的速度追上了你按下 Enter 的速度;Review 要跟得上,審的就不能再是程式碼本身,而是產生程式碼的規則。 Outside-In TDD 之所以有效,是因為它把這條規則限定成「只回應驗收測試」,而不是限定「程式碼本身該長怎樣」。

常見誤區:以為 TDD 只是「先寫測試」

很多人把 TDD 理解成「開發順序調換一下」,這是最常見的誤區。Outside-In TDD 的關顉不在「先寫測試」這個動作,而在只寫測試逼出的最小實作這條紀律本身。如果你先寫了驗收測試,測試綠了之後又忍不住手癢加了一個「以防萬一」的抽象層,那你已經違背了這套方法論的核心精神,測試先行只是形式,過度設計還是會發生。

❌ 常見但走樣的做法:先寫驗收測試,測試通過後繼續「順手」補強架構

測試綠燈之後:
「反正都寫到這了,加個 Repository 介面比較好維護」
「以防之後要換資料庫,包一層 UnitOfWork」
「這個查詢邏輯抽出去做 CQRS 比較乾淨」

✅ 正確做法:測試綠燈就停手,下一個類別要等下一條測試逼出來

測試綠燈之後:
1. 檢查現有程式碼是否有重複可以重構(Refactor 階段)
2. 沒有新的驗收測試要求 → 不新增任何類別
3. 只有當「換資料庫」變成一條真實的驗收測試時,
   才回頭抽出 Repository 介面

這條規則同樣適用於 AI 協作:如果你請 AI 「幫我實作一個發佈文章的功能」,AI 沒有被限制在「只回應這 10 條驗收測試」的紀律下,它就會用它認為「專業」的預設值去填補所有沒講清楚的空白——這正是 Day 2 展示過的現象。Outside-In TDD 對人類工程師有效,對 AI 一樣有效,因為它限制的是「輸出的因果來源」,不是輸出者是人是機器。

今日思考題

回想你自己專案裡最近新增的一個類別:如果有人問你「這個類別是被哪一條測試逼出來的」,你答得出來嗎?如果答不出來,它有沒有可能就是一塊「以防萬一」預先建好、其實從未被真正呼叫過的程式碼?

今日重點回顧

  • Outside-In TDD/ATDD 的核心不是「先寫測試」,而是「只寫測試逼出的最小實作」
  • 一個類別存在的唯一正當理由,是它被某條驗收測試直接逼出來,或是既有類別重構的產物
  • main 版本 3 個檔案、249 行,每一行都能回溯到某條驗收測試的斷言
  • 過度設計版本裡的類別,幾乎都不是任何測試逼出來的,是工程師/AI 憑直覺預先建好的
  • 這套紀律對人類工程師跟 AI coding agent 一樣有效,因為它管的是「因果來源」而不是「執行者」

明日預告

Day 18 會回到這套方法論的源頭:Steve Freeman 跟 Nat Pryce 的《Growing Object-Oriented Software, Guided by Tests》(業界簡稱 GOOS),講清楚它「用外層測試逼出你真正需要的類別」這個核心洞察,跟今天講的 Outside-In TDD 是同一件事的不同說法。

老派工程師的心得

我以前推 TDD 的時候,最常被反駁的一句話是「你這樣寫測試太慢了,先把架構想清楚比較有效率」。這幾年帶 AI 一起寫程式碼之後,我對這句話的看法完全反過來:「先把架構想清楚」聽起來很負責任,實際上往往是「先把不確定的東西都用預留空間包起來」的委婉說法。Outside-In TDD 逼我(或逼 AI)誠實面對一件事——如果一個抽象層沒有任何測試需要它,那它存在的理由就只是我自己的不安全感,不是真正的需求。這件事十幾年前就是真的,AI 時代只是把「不誠實面對」的代價,從「多寫幾個檔案」放大成「多寫幾百個檔案」。


上一篇
Day 16:光讀程式碼找不出的東西——認知負荷與維護成本怎麼衡量
下一篇
Day 18:GOOS 的核心洞察——讓外層測試逼出你真正需要的類別
系列文
當 AI 寫得比你讀得快:Code Review 該審什麼 共 19 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言