章節概述#

上一章為了速度而大量複製程式碼,現在是清理的時候了。本章的目標是將 Dollar 和 Franc 中重複的 equals() 提取到共同的父類別 Money 中。策略是找到共同的父類別(common superclass),逐步把共用邏輯往上搬。

建立 Money 父類別#

首先建立一個空的 Money 類別,讓 Dollar 繼承它:

class Money {
}
class Dollar extends Money {
    private int amount;
}

Figure 6.1: A common superclass for two classes

classDiagram
    class Money {
        #int amount
        +equals(Object) boolean
    }
    class Dollar {
        +Dollar(int amount)
    }
    class Franc {
        +Franc(int amount)
    }
    Money <|-- Dollar
    Money <|-- Franc

接著將 amount 從 Dollar 搬到 Money,並將可見性從 private 改為 protected,讓子類別能存取:

class Money {
    protected int amount;
}
class Dollar extends Money {
}

逐步搬移 equals()#

搬移 equals() 的過程分為多個小步驟,每一步都確保測試通過:

  1. 改變暫時變數的宣告型別:從 Dollar 改為 Money
public boolean equals(Object object) {
    Money dollar = (Dollar) object;
    return amount == dollar.amount;
}
  1. 改變型別轉換:從 (Dollar) 改為 (Money)
public boolean equals(Object object) {
    Money dollar = (Money) object;
    return amount == dollar.amount;
}
  1. 重新命名暫時變數以提高可讀性:
public boolean equals(Object object) {
    Money money = (Money) object;
    return amount == money.amount;
}
  1. 將 equals() 搬到 Money
// Money
public boolean equals(Object object) {
    Money money = (Money) object;
    return amount == money.amount;
}

補寫缺失的測試#

在處理 Franc 之前,作者發現等值性測試中缺少 Franc 對 Franc 的比較。複製程式碼的「罪過」在此顯現——重構時發現測試不足。

注意: 在測試不足的程式碼中實施 TDD 時,你遲早會遇到缺乏測試保護的重構。此時應該先補寫測試,再進行重構。否則可能在重構中引入錯誤而不自知。

public void testEquality() {
    assertTrue(new Dollar(5).equals(new Dollar(5)));
    assertFalse(new Dollar(5).equals(new Dollar(6)));
    assertTrue(new Franc(5).equals(new Franc(5)));
    assertFalse(new Franc(5).equals(new Franc(6)));
}

作者用了一個誇張的因果鏈來強調這個觀點:不補測試 → 重構出錯 → 害怕重構 → 停止重構 → 設計惡化 → 被開除 → 狗離開你 → 不注意營養 → 牙齒壞掉。所以,為了牙齒健康,重構前請補寫測試

對 Franc 做同樣的處理#

有了測試保護後,對 Franc 進行與 Dollar 相同的步驟:

  1. 讓 Franc 繼承 Money
  2. 刪除 Franc 中的 amount 欄位(改用 Money 的)
  3. 逐步將 Franc.equals() 改為與 Money.equals() 完全一致
  4. 刪除 Franc.equals(),因為它與父類別的實作完全相同
class Franc extends Money {
}

測試全部通過,重複的 equals() 已被消除。

新的疑問#

消除 equals() 重複後,一個新問題浮現:如果拿 Franc 和 Dollar 比較會怎樣? 這將在下一章處理。

本章小結#

  • 將共用程式碼從 Dollar 逐步搬移到父類別 Money
  • 讓 Franc 也成為 Money 的子類別
  • 調和(reconcile) 兩個 equals() 實作,確認它們完全一致後才刪除冗餘版本
  • 補寫了重構前缺失的測試