章節概述#

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 將測試程式碼與具體子類別解耦
  • 注意到子類別消失後某些測試可能冗餘,但暫不採取行動