章節概述#
本章的目標是將 Dollar 和 Franc 兩個子類別的 times() 方法統一,推進到只剩單一 Money 類別。過程中遇到了 equals() 的設計問題,必須先修正比較邏輯(從比較類別改為比較幣別),才能完成 times() 的統一。
統一 times() 的起點#
兩個子類別的 times() 實作非常接近,但不完全相同:
// Franc
Money times(int multiplier) {
return Money.franc(amount * multiplier);
}
// Dollar
Money times(int multiplier) {
return Money.dollar(amount * multiplier);
}沒有明顯的方式能直接讓它們相同。Kent Beck 說:「有時你必須往後退才能往前進,就像解魔術方塊一樣。」
反向展開工廠方法#
策略是將剛剛才引入的工廠方法 反向展開(inline),把它們還原為直接建構子呼叫:
// Franc
Money times(int multiplier) {
return new Franc(amount * multiplier, "CHF");
}
// Dollar
Money times(int multiplier) {
return new Dollar(amount * multiplier, "USD");
}因為幣別字串其實就是實體變數 currency,所以可以進一步用變數取代常數:
// Franc
Money times(int multiplier) {
return new Franc(amount * multiplier, currency);
}
// Dollar
Money times(int multiplier) {
return new Dollar(amount * multiplier, currency);
}現在兩個方法幾乎一樣了,差別只在於回傳的是 Franc 還是 Dollar。
用實驗代替推理#
關鍵問題:回傳 Money 而非 Franc 會不會破壞什麼?
重點: Kent Beck 強調,有了測試之後,與其花 5-10 分鐘推理一個問題,不如花 15 秒讓電腦回答你。直接改、跑測試、看結果。沒有測試時你別無選擇只能推理;有了測試,你可以選擇用實驗來回答問題。
嘗試讓 Franc.times() 回傳 Money:
// Franc
Money times(int multiplier) {
return new Money(amount * multiplier, currency);
}編譯器要求 Money 必須是具體類別,因此加入:
class Money {
Money times(int amount) {
return null;
}
}沒有測試的 toString()#
測試跑出紅燈,錯誤訊息不夠清楚。為了改善除錯體驗,直接寫了 toString() 而沒有先寫測試:
public String toString() {
return amount + " " + currency;
}Kent Beck 承認這是例外,理由是:
- 結果馬上就會在螢幕上看到
toString()僅用於除錯輸出,失敗風險很低- 已經在紅燈狀態,不想在紅燈時寫新測試
發現 equals() 的問題#
改善後的錯誤訊息顯示:「expected: <10 CHF> but was: <10 CHF>」——值相同但類別不同。問題出在 equals() 用 getClass() 來比較:
public boolean equals(Object object) {
Money money = (Money) object;
return amount == money.amount
&& getClass().equals(money.getClass());
}應該比較的是幣別(currency),而非類別(class)。
保守策略:退回綠燈再修#
技巧: 在紅燈狀態下不要同時修改多處。保守做法是先退回綠燈,再寫新測試、修正實作,最後重試原始變更。
- 退回:將
Franc.times()還原為回傳new Franc(...),回到綠燈 - 寫測試:驗證不同類別但相同幣別與金額應該相等
public void testDifferentClassEquality() {
assertTrue(new Money(10, "CHF").equals(new Franc(10, "CHF")));
}- 修正 equals():改為比較 currency
public boolean equals(Object object) {
Money money = (Money) object;
return amount == money.amount
&& currency().equals(money.currency());
}- 重試:
Franc.times()和Dollar.times()都可以回傳Money了
flowchart TD
A["反向展開工廠方法"] --> B["實驗:用 Money 取代 Franc"]
B --> C{"測試通過?"}
C -->|"🔴 失敗"| D["發現 equals 比較的是<br/>class 而非 currency"]
D --> E["保守策略:退回綠燈"]
E --> F["寫新測試驗證<br/>幣別比較"]
F --> G["修正 equals():<br/>比較 currency 而非 class"]
G --> H["重新嘗試統一"]統一 times() 到父類別#
兩個子類別的 times() 現在完全相同,可以上移到 Money:
// Money
Money times(int multiplier) {
return new Money(amount * multiplier, currency);
}待辦清單更新,Common times 完成:
Common timesCompare Francs to DollarsCurrency?Delete testFrancMultiplication?
本章小結#
- 用 inline 再替換常數為變數的手法來統一兩個相似方法
- 為除錯而寫了
toString(),沒有先寫測試——這是明確的例外情況 - 嘗試一個變更(回傳
Money而非Franc),讓測試告訴我們是否可行 - 實驗失敗時退回綠燈,補寫測試後再重試,最終成功