章節概述#

待辦清單上還有什麼能幫助消除子類別?本章引入 currency(貨幣) 的概念。透過逐步重構,讓兩個子類別的建構子趨於一致,最終將共同實作推到父類別 Money。

引入 Currency#

從測試開始——我們希望每個 Money 物件都能回報自己的貨幣:

public void testCurrency() {
    assertEquals("USD", Money.dollar(1).currency());
    assertEquals("CHF", Money.franc(1).currency());
}

先在 Money 宣告抽象方法,然後在兩個子類別中實作:

// Money
abstract String currency();

// Dollar
String currency() {
    return "USD";
}

// Franc
String currency() {
    return "CHF";
}

用實例變數取代常數#

為了讓兩個子類別的 currency() 實作統一,將貨幣字串改為存在實例變數中:

// Franc
private String currency;
Franc(int amount) {
    this.amount = amount;
    currency = "CHF";
}
String currency() {
    return currency;
}

Dollar 也做同樣處理。兩邊的 currency 欄位和 currency() 方法現在完全一致,可以推到父類別:

// Money
protected String currency;
String currency() {
    return currency;
}

統一建構子#

下一步是讓建構子也一致。策略是把貨幣常數字串移到工廠方法中,讓建構子接受 currency 參數:

// Franc
Franc(int amount, String currency) {
    this.amount = amount;
    this.currency = currency;
}

工廠方法傳入貨幣字串:

// Money
static Money franc(int amount) {
    return new Franc(amount, "CHF");
}

插曲:修正 times() 中的直接呼叫#

在重構過程中,作者注意到 Franc.times() 直接呼叫建構子而不是工廠方法。雖然不在當前的重構範圍內,但作者允許自己做一個短暫的中斷——修正 times() 讓它使用工廠方法:

// Franc
Money times(int multiplier) {
    return Money.franc(amount * multiplier);
}

技巧: 作者引用 Jim Coplien 的規則——可以接受短暫的中斷,但絕不中斷一個中斷。這是在紀律與務實之間取得平衡的好原則。

接著對 Dollar 做同樣的修改,這次一步到位:

// Money
static Money dollar(int amount) {
    return new Dollar(amount, "USD");
}

// Dollar
Dollar(int amount, String currency) {
    this.amount = amount;
    this.currency = currency;
}
Money times(int multiplier) {
    return Money.dollar(amount * multiplier);
}

一次成功,沒有出錯。

推上共同建構子#

兩個子類別的建構子現在完全一致,可以推到 Money:

// Money
Money(int amount, String currency) {
    this.amount = amount;
    this.currency = currency;
}

// Franc
Franc(int amount, String currency) {
    super(amount, currency);
}

// Dollar
Dollar(int amount, String currency) {
    super(amount, currency);
}

TDD 的步幅調節#

重點: TDD 是一個轉向的過程(steering process)——覺得小步驟太束縛?放大步幅。覺得有點不確定?縮小步幅。沒有永遠正確的步幅大小。作者自己在本章就是先嘗試大步,犯了一個愚蠢錯誤,回退一分鐘的修改,切換到更小的步幅重新來過。

本章小結#

  • 在大設計想法上有點卡住時,先處理之前注意到的小事(引入 currency)
  • 透過將差異移到呼叫端(工廠方法),讓兩個建構子趨於一致
  • 在重構過程中發現小問題(times() 直接呼叫建構子),允許短暫中斷來修正
  • 對 Franc 做的修改,在 Dollar 上以更大的步幅重複一次
  • 將完全一致的建構子推上父類別