本章待辦清單#

  • $5 + 10 CHF = $10(匯率 2:1)
  • $5 + $5 = $10
  • Return Money from $5 + $5
  • Bank.reduce(Money)
  • Reduce Money with conversion
  • Reduce(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 時留下了許多未完成的工作。

策略:先退一步,再前進#

面對大量編譯錯誤,有兩條路:

  1. 寫一個更具體的測試,快速讓它通過,再逐步泛化
  2. 相信編譯器,一次修正所有波及的變更

作者選擇保守路線——先用 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 = $10
  • Return Money from $5 + $5
  • Bank.reduce(Money)
  • Reduce Money with conversion
  • Reduce(Bank, String)
  • Sum.plus
  • Expression.times

所有測試通過。

本章回顧#

  • 先寫理想測試,再退一步:理想版本編譯不過,就先用更具體的型別,確保能一步到位
  • 從葉節點向根部泛化:先改 Sum 的欄位和建構子,再改 Money 的方法,最後才改測試中的宣告
  • 跟隨編譯器的指引:當宣告改為 Expression 時,編譯器會告訴你哪些地方需要連帶修改