章節概述#
上一章用 fake implementation 讓 $5 + $5 = $10 的測試通過了,但 Bank.reduce() 只是硬編碼回傳 Money.dollar(10)。本章的任務是消除這個重複(測試中的 $5 + $5 和實作中的 $10 是同一份資料),讓實作變為真實的。
資料重複,而非程式碼重複#
這次的重複不是傳統的程式碼重複,而是資料重複:
// Bank - fake implementation
Money reduce(Expression source, String to) {
return Money.dollar(10); // 這個 10...
}
// 測試中
Money five = Money.dollar(5);
Expression sum = five.plus(five); // ...等於 5 + 5以往的 fake implementation 可以直接「用變數取代常數」來倒推出真實實作。但這次無法直接倒推,所以改為正向推進(work forward)。
建立 Sum 類別#
Money.plus() 應該回傳真正的 Sum,而非只是 Money。先寫測試:
public void testPlusReturnsSum() {
Money five = Money.dollar(5);
Expression result = five.plus(five);
Sum sum = (Sum) result;
assertEquals(five, sum.augend);
assertEquals(five, sum.addend);
}補充: 加法的第一個參數叫做 augend(被加數),第二個叫 addend(加數)。Kent Beck 說他也是寫書時才知道的。
這個測試深度關注實作細節,預期不會長久存活,但它幫助我們往目標邁進一步。
讓測試通過需要:
// Sum 類別
class Sum implements Expression {
Money augend;
Money addend;
Sum(Money augend, Money addend) {
this.augend = augend;
this.addend = addend;
}
}
// Money.plus() 改為回傳 Sum
Expression plus(Money addend) {
return new Sum(this, addend);
}實作 Bank.reduce() 處理 Sum#
當 Bank.reduce() 收到一個 Sum 時,如果幣別相同,結果應該是金額加總的 Money:
public void testReduceSum() {
Expression sum = new Sum(Money.dollar(3), Money.dollar(4));
Bank bank = new Bank();
Money result = bank.reduce(sum, "USD");
assertEquals(Money.dollar(7), result);
}先寫出能通過的實作:
// Bank
Money reduce(Expression source, String to) {
Sum sum = (Sum) source;
int amount = sum.augend.amount + sum.addend.amount;
return new Money(amount, to);
}這段程式碼有兩個明顯問題:強制轉型(cast) 和公開欄位的多層存取。
重構:將邏輯搬到正確的位置#
把加總邏輯從 Bank 搬到 Sum:
// Bank
Money reduce(Expression source, String to) {
Sum sum = (Sum) source;
return sum.reduce(to);
}
// Sum
public Money reduce(String to) {
int amount = augend.amount + addend.amount;
return new Money(amount, to);
}處理 Bank.reduce(Money) 的情況#
Bank.reduce() 也需要能處理傳入 Money(而非 Sum)的情況:
public void testReduceMoney() {
Bank bank = new Bank();
Money result = bank.reduce(Money.dollar(1), "USD");
assertEquals(Money.dollar(1), result);
}初步實作用 instanceof 判斷:
// Bank
Money reduce(Expression source, String to) {
if (source instanceof Money) return (Money) source;
Sum sum = (Sum) source;
return sum.reduce(to);
}注意: 任何時候看到明確的類別檢查(
instanceof),都應該用多型(polymorphism) 取代。
用多型消除類別檢查#
讓 Money 也實作 reduce():
// Money
public Money reduce(String to) {
return this;
}將 reduce(String) 加入 Expression 介面:
// Expression
interface Expression {
Money reduce(String to);
}這樣 Bank.reduce() 就變得乾淨了:
// Bank
Money reduce(Expression source, String to) {
return source.reduce(to);
}技巧: Kent Beck 提到
Bank.reduce(Expression, String)和Expression.reduce(String)方法名稱相同但參數不同,在 Java 的位置參數語法中不容易表達兩者的差異。在有 keyword parameters 的語言中會更清楚。
本章小結#
- 發現 fake implementation 中的資料重複,決定正向推進而非倒推
- 寫測試來強制建立預期需要的物件(
Sum),逐步讓實作變為真實 - 先用 cast 讓程式跑起來,再透過重構把邏輯搬到正確的類別
- 用多型取代
instanceof,讓Bank.reduce()變得簡潔 - 待辦清單新增了「Reduce Money with conversion」和「Reduce(Bank, String)」,為下一步的幣別轉換做準備