本章待辦清單#

  • $5 + 10 CHF = $10(匯率 2:1)
  • $5 + $5 = $10
  • Return Money from $5 + $5
  • Bank.reduce(Money)
  • Reduce Money with conversion
  • Reduce(Bank, String)

匯率轉換測試#

本章的目標是實作貨幣轉換:將 2 法郎換成 1 美元。直接寫成測試:

public void testReduceMoneyDifferentCurrency() {
    Bank bank = new Bank();
    bank.addRate("CHF", "USD", 2);
    Money result = bank.reduce(Money.franc(2), "USD");
    assertEquals(Money.dollar(1), result);
}

快速讓測試通過#

最直接的做法是在 Money.reduce() 中硬編碼匯率:

public Money reduce(String to) {
    int rate = (currency.equals("CHF") && to.equals("USD"))
        ? 2
        : 1;
    return new Money(amount / rate, to);
}

這能讓測試通過,但問題很明顯——Money 不應該知道匯率。匯率應該是 Bank 的責任。

將 Bank 傳入 reduce()#

為了讓 Bank 負責匯率,需要把 Bank 作為參數傳入 Expression.reduce()。依序修改:

呼叫端(Bank):

Money reduce(Expression source, String to) {
    return source.reduce(this, to);
}

介面宣告(Expression):

Money reduce(Bank bank, String to);

Sum 的實作:

public Money reduce(Bank bank, String to) {
    int amount = augend.amount + addend.amount;
    return new Money(amount, to);
}

Money 的實作(暫時仍硬編碼匯率):

public Money reduce(Bank bank, String to) {
    int rate = (currency.equals("CHF") && to.equals("USD"))
        ? 2
        : 1;
    return new Money(amount / rate, to);
}

補充: 這些方法必須宣告為 public,因為 Java 介面的方法必須是 public。

在 Bank 中實作匯率查詢#

接著在 Bank 新增 rate() 方法,把匯率邏輯搬過去:

int rate(String from, String to) {
    return (from.equals("CHF") && to.equals("USD"))
        ? 2
        : 1;
}

Money 改為向 Bank 查詢匯率:

public Money reduce(Bank bank, String to) {
    int rate = bank.rate(currency, to);
    return new Money(amount / rate, to);
}

用 Hashtable 儲存匯率#

硬編碼的 2 同時出現在測試和程式碼中,這是重複。為了消除它,需要在 Bank 中用表格存放匯率。

首先確認能否用陣列作為 Hashtable 的 key——寫個測試驗證:

public void testArrayEquals() {
    assertEquals(new Object[] {"abc"}, new Object[] {"abc"});
}

測試失敗,Array.equals() 不會比較元素內容。因此需要自定義一個 Pair 類別作為 key:

private class Pair {
    private String from;
    private String to;

    Pair(String from, String to) {
        this.from = from;
        this.to = to;
    }

    public boolean equals(Object object) {
        Pair pair = (Pair) object;
        return from.equals(pair.from) && to.equals(pair.to);
    }

    public int hashCode() {
        return 0;
    }
}

補充: hashCode() 回傳 0 是很糟糕的雜湊值,但實作簡單且能正確運作,查詢會退化為線性搜尋。等到有大量貨幣時再優化。作者沒有為 Pair 寫獨立測試,因為它是在重構過程中引入的——如果重構完成後所有測試都通過,就表示程式碼已被充分驗證。

Bank 中新增匯率儲存和查詢:

private Hashtable rates = new Hashtable();

void addRate(String from, String to, int rate) {
    rates.put(new Pair(from, to), new Integer(rate));
}

int rate(String from, String to) {
    Integer rate = (Integer) rates.get(new Pair(from, to));
    return rate.intValue();
}

意外的紅燈:同幣種匯率#

執行後出現紅燈!調查發現,查詢 USD 到 USD 的匯率時回傳了 null(因為表格中沒有這筆資料),但我們期望結果是 1

既然這是個意外發現,就為它寫一個測試來記錄:

public void testIdentityRate() {
    assertEquals(1, new Bank().rate("USD", "USD"));
}

修正方式是在 rate() 中加入同幣種的特殊處理:

int rate(String from, String to) {
    if (from.equals(to)) return 1;
    Integer rate = (Integer) rates.get(new Pair(from, to));
    return rate.intValue();
}

綠燈!

重點: 當重構過程中遇到意外失敗,不要盲目前進。寫一個新測試來隔離問題,這樣既能修正錯誤,又能防止未來再犯。

本章回顧#

  • 快速加入預期需要的參數:幾秒內就把 Bank 作為參數加入 reduce()
  • 消除測試與程式碼之間的資料重複:匯率 2 從硬編碼改為表格查詢
  • 寫測試驗證假設:用 testArrayEquals 驗證 Java 陣列的行為
  • 引入私有輔助類別Pair 沒有寫獨立測試
  • 重構中遭遇錯誤時選擇寫新測試來隔離問題,而非盲目修正