本章待辦清單#

  • $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.plus()#

測試用例:

public void testSumPlusMoney() {
    Expression fiveBucks = Money.dollar(5);
    Expression tenFrancs = Money.franc(10);
    Bank bank = new Bank();
    bank.addRate("CHF", "USD", 2);
    Expression sum = new Sum(fiveBucks, tenFrancs).plus(fiveBucks);
    Money result = bank.reduce(sum, "USD");
    assertEquals(Money.dollar(15), result);
}

補充: 這裡刻意用 new Sum(...) 而非 fiveBucks.plus(tenFrancs) 來建立 Sum,是為了讓測試意圖更直接明確。測試不只是讓程式能跑,更是寫給未來讀者看的文件。

實作非常簡單,和 Money.plus() 的邏輯完全一樣:

public Expression plus(Expression addend) {
    return new Sum(this, addend);
}

技巧: Sum.plus() 和 Money.plus() 的程式碼完全相同,暗示未來可以抽取到共同的抽象類別中。

TDD 的經濟學#

作者指出:使用 TDD 時,測試程式碼和正式程式碼的行數大致相當。要讓 TDD 在經濟上合理,你需要:

  • 每天寫出兩倍的程式碼量,或
  • 用一半的程式碼量完成相同功能

在評估時,別忘了把除錯、整合、解釋的時間也算進去

實作 Sum.times()#

一旦 Sum.times() 能運作,在 Expression 介面宣告 times() 就只是簡單一步。

測試用例:

public void testSumTimes() {
    Expression fiveBucks = Money.dollar(5);
    Expression tenFrancs = Money.franc(10);
    Bank bank = new Bank();
    bank.addRate("CHF", "USD", 2);
    Expression sum = new Sum(fiveBucks, tenFrancs).times(2);
    Money result = bank.reduce(sum, "USD");
    assertEquals(Money.dollar(20), result);
}

Sum.times() 的實作——對兩個加數分別做 times:

Expression times(int multiplier) {
    return new Sum(augend.times(multiplier), addend.times(multiplier));
}

因為上一章已把 augend 和 addend 抽象為 Expression,現在必須在 Expression 介面宣告 times()

Expression times(int multiplier);

這迫使 Money.times() 和 Sum.times() 都改為 public:

// Sum
public Expression times(int multiplier) {
    return new Sum(augend.times(multiplier), addend.times(multiplier));
}

// Money
public Expression times(int multiplier) {
    return new Money(amount * multiplier, currency);
}

測試通過。

更新後的待辦清單#

  • $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

所有項目完成!

嘗試性實驗:$5 + $5 回傳 Money#

最後一個鬆散的想法:當兩個同幣種相加時,能否直接回傳 Money 而非 Sum?

public void testPlusSameCurrencyReturnsMoney() {
    Expression sum = Money.dollar(1).plus(Money.dollar(1));
    assertTrue(sum instanceof Money);
}

注意: 這個測試不太好,因為它在測試實作細節(instanceof)而非外部可見行為。

檢視需要修改的程式碼:

public Expression plus(Expression addend) {
    return new Sum(this, addend);
}

沒有明顯、乾淨的方法能在 addend 是 Money 時檢查其幣種。實驗失敗,刪除測試,繼續前進。

本章回顧#

  • 為未來讀者寫測試:測試是活文件,要讓意圖清楚
  • 建議嘗試 TDD 與你現有開發方式的對比實驗
  • 再一次跟隨編譯器的連鎖反應:改了宣告,編譯器會告訴你下一步該改什麼
  • 進行短暫實驗,失敗就果斷放棄:不要硬撐不好的方向