章節概述#
本章終於開始處理多幣別加法——整個 Money 範例的核心目標。Kent Beck 從一個較簡單的測試案例($5 + $5)切入,並引入了關鍵的設計隱喻:Expression(表達式)。這個隱喻將深刻影響後續的架構。
從簡單案例開始#
原始目標 $5 + 10 CHF = $10 if rate is 2:1 太複雜,於是先從同幣別加法開始:
public void testSimpleAddition() {
Money sum = Money.dollar(5).plus(Money.dollar(5));
assertEquals(Money.dollar(10), sum);
}實作看起來很直觀,直接上:
// Money
Money plus(Money addend) {
return new Money(amount + addend.amount, currency);
}補充: Kent Beck 提到,他會開始加快實作速度以節省篇幅。但在設計不明顯的地方,仍會 fake implementation 再重構。TDD 讓你可以控制步伐的大小。
設計思考:如何表示多幣別運算#
最困難的設計約束是:系統中大部分程式碼不應該知道自己可能在處理多種幣別。
兩種可能的策略:
- 立即換算為參考幣別:簡單但不易支援匯率變動
- Expression 隱喻:將運算過程保留為表達式樹,到最後再根據匯率化約(reduce)為單一幣別
Kent Beck 選擇了第二種。
Expression 隱喻#
核心概念:
- Money 是表達式的原子形式(atomic form)
- 運算產生 Expression,其中一種是 Sum
- 完成運算後,Expression 可以根據一組匯率被 reduce 回單一幣別
- 負責 reduce 的角色是 Bank
重點: Ward Cunningham 發明的這個 imposter 技巧——當物件行為不符合需求時,建立一個具有相同外部協定但不同實作的物件。TDD 無法保證你在正確時機獲得設計靈感,但有信心的測試和精心分解的程式碼,能為靈感做好準備。
classDiagram
class Expression {
<<interface>>
+reduce(Bank bank, String to) Money
}
class Money {
#int amount
#String currency
+plus(Money addend) Expression
+times(int multiplier) Money
+reduce(Bank bank, String to) Money
}
class Sum {
+Money augend
+Money addend
+reduce(Bank bank, String to) Money
}
class Bank {
+reduce(Expression source, String to) Money
+addRate(String from, String to, int rate)
}
Expression <|.. Money
Expression <|.. Sum
Bank ..> Expression : reduces逐步建構測試#
從結果往回推,一步步建構完整的測試:
public void testSimpleAddition() {
Money five = Money.dollar(5);
Expression sum = five.plus(five);
Bank bank = new Bank();
Money reduced = bank.reduce(sum, "USD");
assertEquals(Money.dollar(10), reduced);
}為什麼讓 Bank 負責 reduce#
Kent Beck 解釋了將 reduce 責任放在 Bank 而非 Expression 上的考量:
- Expression 是核心物件,希望它盡可能不知道外部世界,以保持彈性、易測試、易重用
- 可預見 Expression 會有很多操作,如果全加到 Expression 上,它會無限膨脹
不過他也強調,如果後來發現 Bank 不需要參與,隨時可以把責任搬回 Expression。
讓測試通過#
需要建立的型別與方法:
// Expression 介面
interface Expression
// Money 實作 Expression
class Money implements Expression {
Expression plus(Money addend) {
return new Money(amount + addend.amount, currency);
}
}
// Bank 類別
class Bank {
Money reduce(Expression source, String to) {
return Money.dollar(10); // 先用 fake implementation
}
}測試通過(綠燈),但 Bank.reduce() 只是硬編碼回傳值,真正的實作留待下一章。
本章小結#
- 將大測試($5 + 10 CHF)拆解為較小的測試($5 + $5),代表一個可管理的進展
- 仔細思考運算的隱喻,選擇了 Expression 模型
- 根據新隱喻重寫測試,快速讓它通過編譯和執行
- 用 fake implementation 先拿到綠燈,真正的重構留到後面