章節概述#
本章處理 Dollar 物件的 side effect 問題——呼叫 times() 會改變原始物件的值。作者透過改變介面讓 times() 回傳新物件來解決問題,並藉此介紹 TDD 中快速讓測試變綠的三種策略。
TDD 的一般循環#
作者在本章開頭完整闡述了 TDD 的三階段循環:
寫一個測試(Write a test):想像你希望操作在程式碼中呈現的樣貌,你在寫一個故事。設計你理想中的介面,包含計算正確答案所需的所有元素。
讓它通過(Make it run):快速讓進度條變綠,這是最高優先。如果乾淨簡單的解法顯而易見就直接寫;如果需要花一點時間,先記下來,先用最快方式讓測試通過。快速綠燈可以暫時赦免一切罪過,但只有一瞬間。
讓它正確(Make it right):系統行為正確後,回頭消除剛才引入的重複,走回軟體正道。
重點: 目標是「能運作的乾淨程式碼」(clean code that works)。策略是分而治之——先解決「能運作」,再解決「乾淨」。這與架構驅動開發(先求乾淨再設法讓它動)的方向恰好相反。
發現 Side Effect 問題#
上一章的 Dollar 有一個問題:呼叫 times() 會修改原始物件。如果我們嘗試對同一個 five 連續呼叫兩次乘法:
public void testMultiplication() {
Dollar five = new Dollar(5);
five.times(2);
assertEquals(10, five.amount);
five.times(3);
assertEquals(15, five.amount);
}第二個 assertion 會失敗,因為執行完 five.times(2) 後,five 的值已經變成 10 了,再乘以 3 會得到 30,而非預期的 15。
解法:回傳新物件#
找不到讓現有測試乾淨通過的方法,所以作者改變了介面——讓 times() 回傳一個新的 Dollar 物件,而非修改原始物件。測試隨之更新:
public void testMultiplication() {
Dollar five = new Dollar(5);
Dollar product = five.times(2);
assertEquals(10, product.amount);
product = five.times(3);
assertEquals(15, product.amount);
}補充: 改變介面沒關係。對介面的猜測不會比對實作的猜測更完美——發現更好的設計時就該調整。
先讓測試能編譯——times() 改為回傳 Dollar,暫時 return null:
Dollar times(int multiplier) {
amount *= multiplier;
return null;
}測試能編譯了但會失敗——這就是進步。接著直接寫出正確的實作:
Dollar times(int multiplier) {
return new Dollar(amount * multiplier);
}測試通過,綠燈亮起。
三種快速讓測試變綠的策略#
本章介紹了三種策略中的兩種(第三種在 Chapter 3 示範):
- Fake It(假裝):先回傳一個常數,然後逐步用變數取代常數,直到得出真正的程式碼。Chapter 1 就是用這個策略。
- Obvious Implementation(顯而易見的實作):當你清楚知道正確的實作是什麼,就直接寫上去。本章用的就是這個策略。
技巧: 在實務中,這兩種模式會交替使用。一切順利時,連續使用 Obvious Implementation(每次都跑測試確認)。一旦遇到意外的紅燈,就退回 Fake It 模式,透過重構找到正確的程式碼。信心恢復後,再切回 Obvious Implementation。
flowchart TD
A["寫一個失敗的測試"] --> B{"有信心直接實作?"}
B -->|"是"| C["Obvious Implementation<br/>直接寫出正確實作"]
B -->|"否"| D["Fake It<br/>先回傳常數"]
C --> E{"測試通過?"}
D --> E
E -->|"🟢 通過"| F["重構消除重複"]
E -->|"🔴 失敗"| G["退回 Fake It"]
G --> D
F --> A將感覺轉化為測試#
重點: 將直覺感受(例如對 side effect 的不舒服感)轉化為具體的測試案例(例如對同一個 Dollar 乘兩次),是 TDD 中反覆出現的主題。隨著練習越多,你越能將美學判斷轉化為測試。當你能做到這一點,設計討論就會變得更有趣——先討論系統應該如何運作,決定正確行為後,再討論實現的最佳方式。
本章小結#
本章的要點回顧:
- 將一個設計上的不滿(side effect)轉化為一個會因此失敗的測試案例
- 用 stub 實作快速讓測試編譯通過
- 直接寫入看似正確的實作來讓測試通過
- 介紹了 Fake It 與 Obvious Implementation 兩種策略