章節概述#
Dollar 和 Franc 的 times() 實作幾乎一模一樣,本章要朝著消除這個重複邁進。策略是引入 Factory Method(工廠方法),讓測試程式碼不再直接引用子類別,為未來消除子類別鋪路。
times() 的重複#
兩個子類別的 times() 實作極為相似:
// Franc
Franc times(int multiplier) {
return new Franc(amount * multiplier);
}
// Dollar
Dollar times(int multiplier) {
return new Dollar(amount * multiplier);
}第一步是將回傳型別統一為 Money:
// Franc
Money times(int multiplier) {
return new Franc(amount * multiplier);
}
// Dollar
Money times(int multiplier) {
return new Dollar(amount * multiplier);
}引入 Factory Method#
下一步的方向不太明顯。兩個子類別做的事太少,不足以證明它們存在的價值,理想上應該消除它們。但一步到位不符合 TDD 的精神。
作者採取的策略是:減少外部對子類別的直接引用。具體做法是在 Money 上建立工廠方法:
// Money
static Money dollar(int amount) {
return new Dollar(amount);
}測試改為使用工廠方法:
public void testMultiplication() {
Money five = Money.dollar(5);
assertEquals(Money.dollar(10), five.times(2));
assertEquals(Money.dollar(15), five.times(3));
}由於 five 的宣告型別改為 Money,編譯器要求 Money 有 times() 方法。因此將 Money 宣告為抽象類別,並加上抽象方法:
abstract class Money {
abstract Money times(int multiplier);
}重點: 透過工廠方法,測試程式碼不再知道 Dollar 這個子類別的存在。這帶來了一個重要的好處——我們可以自由改變繼承結構,而不影響任何模型程式碼。
classDiagram
class Money {
<<abstract>>
#int amount
+times(int multiplier)* Money
+equals(Object) boolean
+dollar(int amount)$ Money
+franc(int amount)$ Money
}
class Dollar {
+times(int multiplier) Money
}
class Franc {
+times(int multiplier) Money
}
Money <|-- Dollar
Money <|-- Franc
Money ..> Dollar : creates
Money ..> Franc : creates對 Franc 做同樣處理#
同樣地,為 Franc 建立工廠方法,並更新所有測試:
// Money
static Money franc(int amount) {
return new Franc(amount);
}public void testEquality() {
assertTrue(Money.dollar(5).equals(Money.dollar(5)));
assertFalse(Money.dollar(5).equals(Money.dollar(6)));
assertTrue(Money.franc(5).equals(Money.franc(5)));
assertFalse(Money.franc(5).equals(Money.franc(6)));
assertFalse(Money.franc(5).equals(Money.dollar(5)));
}
public void testFrancMultiplication() {
Money five = Money.franc(5);
assertEquals(Money.franc(10), five.times(2));
assertEquals(Money.franc(15), five.times(3));
}冗餘測試的觀察#
作者注意到 testFrancMultiplication 並沒有測試任何 testMultiplication(Dollar 版本)尚未涵蓋的邏輯。如果刪掉它,我們會失去信心嗎?目前還有一點,所以暫時保留。但將「Delete testFrancMultiplication?」加入待辦清單。
技巧: 當你對某個測試的必要性產生懷疑時,不必急著刪除。把它記在待辦清單上,等子類別真正被消除後再回來審視。
本章小結#
- 將兩個
times()的方法簽名統一(回傳型別改為 Money),朝消除重複邁進 - 將方法宣告搬到共同的父類別(abstract method)
- 引入 Factory Method 將測試程式碼與具體子類別解耦
- 注意到子類別消失後某些測試可能冗餘,但暫不採取行動