本章待辦清單#
- $5 + 10 CHF = $10(匯率 2:1)
$5 + $5 = $10Return Money from $5 + $5Bank.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沒有寫獨立測試 - 重構中遭遇錯誤時選擇寫新測試來隔離問題,而非盲目修正