本章待辦清單#
- $5 + 10 CHF = $10(匯率 2:1)
$5 + $5 = $10Return Money from $5 + $5Bank.reduce(Money)Reduce Money with conversionReduce(Bank, String)
終於來到混合貨幣加法#
這是從一開始就驅動整個設計的測試——$5 + 10 CHF:
public void testMixedAddition() {
Expression fiveBucks = Money.dollar(5);
Expression tenFrancs = Money.franc(10);
Bank bank = new Bank();
bank.addRate("CHF", "USD", 2);
Money result = bank.reduce(fiveBucks.plus(tenFrancs), "USD");
assertEquals(Money.dollar(10), result);
}但這個理想版本無法直接編譯,因為之前從 Money 泛化到 Expression 時留下了許多未完成的工作。
策略:先退一步,再前進#
面對大量編譯錯誤,有兩條路:
- 寫一個更具體的測試,快速讓它通過,再逐步泛化
- 相信編譯器,一次修正所有波及的變更
作者選擇保守路線——先用 Money 型別寫測試:
public void testMixedAddition() {
Money fiveBucks = Money.dollar(5);
Money tenFrancs = Money.franc(10);
Bank bank = new Bank();
bank.addRate("CHF", "USD", 2);
Money result = bank.reduce(fiveBucks.plus(tenFrancs), "USD");
assertEquals(Money.dollar(10), result);
}修正 Sum.reduce()#
測試失敗,得到 15 USD 而非 10 USD。原因是 Sum.reduce() 沒有對加數做貨幣轉換——它直接把金額相加而忽略了匯率。
修正前:
public Money reduce(Bank bank, String to) {
int amount = augend.amount + addend.amount;
return new Money(amount, to);
}修正後——對兩個加數分別做 reduce:
public Money reduce(Bank bank, String to) {
int amount = augend.reduce(bank, to).amount
+ addend.reduce(bank, to).amount;
return new Money(amount, to);
}測試通過。
從葉節點向根部泛化#
現在開始逐步把 Money 替換為 Expression,策略是從邊緣(葉節點)往回推到測試(根部),以避免變更的連鎖反應。
Step 1:Sum 的欄位改為 Expression
Expression augend;
Expression addend;Step 2:Sum 建構子參數改為 Expression
Sum(Expression augend, Expression addend) {
this.augend = augend;
this.addend = addend;
}Step 3:Money.plus() 的參數改為 Expression
Expression plus(Expression addend) {
return new Sum(this, addend);
}Step 4:Money.times() 回傳 Expression
Expression times(int multiplier) {
return new Money(amount * multiplier, currency);
}技巧: 這暗示 Expression 介面應該包含
plus()和times()操作。
逐步修改測試中的型別宣告#
先把 tenFrancs 改為 Expression:
public void testMixedAddition() {
Money fiveBucks = Money.dollar(5);
Expression tenFrancs = Money.franc(10);
Bank bank = new Bank();
bank.addRate("CHF", "USD", 2);
Money result = bank.reduce(fiveBucks.plus(tenFrancs), "USD");
assertEquals(Money.dollar(10), result);
}再把 fiveBucks 也改為 Expression——這會觸發一連串編譯錯誤,因為 Expression 還沒有定義 plus():
public void testMixedAddition() {
Expression fiveBucks = Money.dollar(5);
Expression tenFrancs = Money.franc(10);
Bank bank = new Bank();
bank.addRate("CHF", "USD", 2);
Money result = bank.reduce(fiveBucks.plus(tenFrancs), "USD");
assertEquals(Money.dollar(10), result);
}跟隨編譯器修正#
在 Expression 介面加入 plus():
Expression plus(Expression addend);Money 中的 plus() 必須改為 public:
public Expression plus(Expression addend) {
return new Sum(this, addend);
}Sum 先用 stub 實作,加入待辦清單:
public Expression plus(Expression addend) {
return null;
}更新後的待辦清單#
$5 + 10 CHF = $10(匯率 2:1)$5 + $5 = $10Return Money from $5 + $5Bank.reduce(Money)Reduce Money with conversionReduce(Bank, String)- Sum.plus
- Expression.times
所有測試通過。
本章回顧#
- 先寫理想測試,再退一步:理想版本編譯不過,就先用更具體的型別,確保能一步到位
- 從葉節點向根部泛化:先改 Sum 的欄位和建構子,再改 Money 的方法,最後才改測試中的宣告
- 跟隨編譯器的指引:當宣告改為 Expression 時,編譯器會告訴你哪些地方需要連帶修改