iT邦幫忙

2026 iThome 鐵人賽

DAY 18
0
Software Development

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

Day 18:GOOS 的核心洞察——讓外層測試逼出你真正需要的類別

  • 分享至 

  • xImage
  •  

前言:「這套方法論該不會是你自己發明的吧?」

昨天講完 Outside-In TDD/ATDD 怎麼擋住過度設計,這裡有一個很合理的質疑:這聽起來很像是「先射箭再畫靶」——先做出一個乾淨案例,再回頭找一套聽起來很有道理的理論幫它背書。所以今天要老實回答一個問題:這套方法論,不是我這個系列發明的,也不是為了反駁 AI 才生出來的新概念,它有一個明確的出處。

出處是 Steve Freeman 跟 Nat Pryce 合著的《Growing Object-Oriented Software, Guided by Tests》,業界通常簡稱 GOOS(有些場合也會唸成「goose」),2009 年由 Addison-Wesley 出版。這本書在敏捷/TDD 社群裡是被廣泛引用的經典之一,核心方法論被稱為「Outside-In TDD」或「London school TDD」。今天不打算逐章導讀這本書(我也沒有把握能精準複述書裡每一個細節),而是聚焦在一個對這個系列最重要的洞察上:讓最外層的測試,去逼出你真正需要的類別跟協作關係,而不是先畫好架構圖再補測試。

今日目標

  • 認識 GOOS 這本書的作者、書名跟核心方法論的名稱,避免記錯基本事實
  • 理解「outside-in」在 GOOS 語境下具體指什麼方向:從系統邊界往內推進
  • 理解 GOOS 為什麼特別強調用測試「發現」物件之間的協作關係,而不是預先設計
  • 看懂這個洞察跟這個系列案例(main 版本 3 個檔案)之間的直接對應關係
  • 建立一個保守但正確的認知邊界:哪些是這本書明確講的,哪些是後續社群的延伸詮釋

GOOS 的核心洞察:測試不只驗證行為,還「發現」設計

GOOS 這本書處理的問題,跟這個系列處理的問題幾乎是同一個:怎麼避免寫出一堆「看起來合理但其實沒必要」的物件跟抽象層。書裡提出的解法方向,是從系統最外層(例如一個使用者可觀察的端到端行為)開始寫測試,讓這個測試在執行過程中,一步步「逼」出你需要哪些協作物件、這些物件之間該怎麼溝通——而不是工程師先在紙上或腦中設計好一整套類別圖,再回頭補測試證明它是對的。

這跟昨天講的 Outside-In TDD 是同一件事的不同角度:Outside-In TDD 講的是「開發順序」(先寫外層測試,逐步往內推進),GOOS 講的是「這個順序背後的設計哲學」——測試不只是拿來驗證程式碼有沒有 bug 的工具,它同時是一種發現設計的手段。你在寫測試的過程中,會自然地問「這個物件需要跟誰溝通才能完成這件事」,而不是預先假設「將來可能需要跟誰溝通」。

這正是這個系列的主題句在方法論源頭的體現:AI 沒有發明過度設計,它只是讓過度設計的速度追上了你按下 Enter 的速度;Review 要跟得上,審的就不能再是程式碼本身,而是產生程式碼的規則。 GOOS 早在 AI 出現前十幾年,就已經在回答「怎麼讓測試逼出真正需要的設計,而不是讓工程師的直覺搶先一步」這個問題。

為什麼這個順序很重要

如果順序反過來——先設計架構、再寫測試去符合這個架構——測試就只能驗證「這個架構有沒有照我想的方式運作」,沒有辦法反過來質疑「這個架構真的有必要嗎」。這也是我在這個系列前面反覆強調「測試通過不等於設計正確」的原因:如果一開始的架構就是預先想像出來的,那麼再多測試,也只是在幫這個想像出來的架構背書,不會有任何一條測試去指出「這一層其實是多餘的」。

GOOS 的做法把因果關係倒過來:先有一個必須讓測試通過的具體壓力,再由這個壓力去逼出「剛好夠用」的設計。這跟這個系列案例裡乾淨版本的產生過程完全對應——main 版本的 ArticleRepository、PublishArticleService 不是先設計出來的架構藍圖,而是 10 條驗收測試依序逼出來的最小集合。

常見誤區:把 GOOS 簡化成「多寫 mock」

在中文技術圈,GOOS 有時候會被簡化成「教你怎麼用 mock object 寫測試」的書,這是一個容易失焦的理解。Mock(或更廣義的 test double)在 GOOS 裡確實是重要的技術工具,用來在還沒有真正實作協作物件之前,先描述「這個物件應該跟誰溝通、溝通什麼內容」,但工具本身不是重點,重點是這個工具服務於「讓外層測試逼出內層設計」這個更根本的目的。如果只學會了「多寫 mock」這個技巧,卻沒有理解它背後為什麼要這樣做,很容易變成另一種形式的過度設計——為每一個協作關係都預先建一層介面跟 mock,卻沒有回頭問「這個協作關係是不是真的被某條外層測試要求」。

❌ 誤解:GOOS = 多用 mock,測試每個類別的每個方法

為 ArticleRepository 寫一個 mock,
測試 PublishArticleService 是否呼叫了 save()、
測試順序是否正確、呼叫次數是否正確……
即使根本沒有一條驗收測試在乎這些內部呼叫細節

✅ 正確方向:外層驗收測試先行,內層協作關係是測試逼出來的副產品,不是目的本身

先寫「發佈已發佈的文章要拋例外」這條驗收測試,
測試變綠所需要的最小協作關係
(Service 需要跟 Repository 溝通),
才是需要被建出來的東西——
不多加一層它沒要求的抽象。

一個保守的但重要的提醒

我不打算在這篇文章裡假裝自己能精準複述 GOOS 書裡每一個章節安排或原文用詞——如果你想深入了解書裡完整的方法論(例如「walking skeleton」這類具體實踐手法),我建議直接找原書或社群裡經過查證的導讀來看,這篇文章只負責把「核心洞察」這一層講對、講清楚,避免我在不確定的細節上誤導讀者。

今日思考題

回想你上一個用 TDD 開發的功能:你的測試是「先逼出設計」,還是「先有設計、測試只是拿來確認它符合預期」?如果是後者,你有沒有可能已經錯過了 GOOS 這個洞察真正想解決的問題?

今日重點回顧

  • GOOS 是 Steve Freeman 與 Nat Pryce 合著的《Growing Object-Oriented Software, Guided by Tests》,2009 年出版,是 Outside-In TDD/London school TDD 的重要參考來源
  • 核心洞察:讓最外層測試逼出你需要的協作物件與設計,而不是先設計架構再補測試證明它對
  • Mock/test double 是服務這個洞察的工具,不是目的本身;只學工具容易變成另一種過度設計
  • 這個系列案例的乾淨版本,因果順序完全對應 GOOS 描述的做法:測試先行,設計是被逼出來的結果

明日預告

Day 19 要把「不要過度設計」這件事從方法論落地成可執行的規則——怎麼用架構測試工具,把「依賴層數不能超過幾層」「不能出現某些 pattern」這類約束寫成 CI 會自動擋下來的檢查,而不是只能靠人 review 時憑印象抓。

老派工程師的心得

我第一次接觸 GOOS 這本書的時候,其實沒有馬上理解「測試逼出設計」跟「測試驗證設計」這兩者的差別有多大——直到我自己動手做這個系列的案例,親眼看著同一組測試,一邊逼出 3 個檔案的乾淨版本,一邊被拿來「事後驗證」一個 745 個檔案的過度設計版本,我才真正理解這個順序上的差異有多致命。測試在哪個時間點介入,決定了它是在幫你發現真相,還是在幫你的直覺背書——這句話,我現在會說,是這幾天重新整理這個案例之後,對 GOOS 最深的一層體會。


上一篇
Day 17:Outside-In TDD/ATDD——最古老的過度設計解藥
下一篇
Day 19:把「不要過度設計」寫成可執行的架構測試
系列文
當 AI 寫得比你讀得快:Code Review 該審什麼 共 19 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言