章節概述#

本章終於開始處理多幣別加法——整個 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 先拿到綠燈,真正的重構留到後面