前置脈絡#
在 legacy code 中新增功能時,最重要的考量是我們沒有測試。Chapter 6 的 sprout 和 wrap 技巧可以在沒有測試的情況下加入程式碼,但存在一些隱患:
- 不會顯著改善既有程式碼
- 可能引入重複(新程式碼與未測試區域的舊程式碼重複)
- 恐懼與放棄是兩大心態危害
如果能將程式碼放入測試,就應該使用本章的技巧正面面對。Sprout 和 wrap 留在程式碼中的痕跡,是對我們當時無法做得更好的小小提醒。
Test-Driven Development (TDD)#
TDD 是作者認為最強大的功能新增技巧。核心演算法:
- Write a failing test case(寫一個失敗的測試)
- Get it to compile(讓它通過編譯)
- Make it pass(讓測試通過)
- Remove duplication(移除重複)
- Repeat(重複)
TDD 範例:InstrumentCalculator#
需求:計算 first statistical moment about a point。
第一輪 – 寫失敗的測試:
public void testFirstMoment() {
InstrumentCalculator calculator = new InstrumentCalculator();
calculator.addElement(1.0);
calculator.addElement(2.0);
assertEquals(-0.5, calculator.firstMomentAbout(2.0), TOLERANCE);
}讓它編譯 – 加入空方法:
public class InstrumentCalculator {
double firstMomentAbout(double point) {
return Double.NaN;
}
...
}讓測試通過:
public double firstMomentAbout(double point) {
double numerator = 0.0;
for (Iterator it = elements.iterator(); it.hasNext(); ) {
double element = ((Double)(it.next())).doubleValue();
numerator += element - point;
}
return numerator / elements.size();
}這在 TDD 中是異常大量的程式碼。通常步驟應該更小,除非你對演算法非常確定。
第二輪 – 處理邊界情況(除以零):
public void testFirstMoment() {
try {
new InstrumentCalculator().firstMomentAbout(0.0);
fail("expected InvalidBasisException");
}
catch (InvalidBasisException e) {
}
}加入例外處理後測試通過。
第三輪 – secondMomentAbout:
先複製 firstMomentAbout 並修改為 secondMomentAbout:
public double secondMomentAbout(double point)
throws InvalidBasisException {
if (elements.size() == 0)
throw new InvalidBasisException("no elements");
double numerator = 0.0;
for (Iterator it = elements.iterator(); it.hasNext(); ) {
double element = ((Double)(it.next())).doubleValue();
numerator += Math.pow(element - point, 2.0);
}
return numerator / elements.size();
}移除重複 – 抽取通用方法:
public double secondMomentAbout(double point)
throws InvalidBasisException {
return nthMomentAbout(point, 2.0);
}
private double nthMomentAbout(double point, double n)
throws InvalidBasisException {
if (elements.size() == 0)
throw new InvalidBasisException("no elements");
double numerator = 0.0;
for (Iterator it = elements.iterator(); it.hasNext(); ) {
double element = ((Double)(it.next())).doubleValue();
numerator += Math.pow(element - point, n);
}
return numerator / elements.size();
}然後讓 firstMomentAbout 也委託給 nthMomentAbout:
public double firstMomentAbout(double point)
throws InvalidBasisException {
return nthMomentAbout(point, 1.0);
}TDD 與 Legacy Code#
TDD 最有價值的一點是它讓你一次專注一件事。你不是同時寫程式碼和重構;你只做其中一件。
針對 legacy code,TDD 演算法可以擴展為:
- Get the class you want to change under test(將目標 class 放入測試)
- Write a failing test case
- Get it to compile
- Make it pass(盡量不要修改既有程式碼)
- Remove duplication
- Repeat
Programming by Difference#
Programming by Difference 是一種利用繼承來快速引入變化的技巧。在 OO 中,我們可以透過子類別化來新增功能,而不需修改原有 class。
範例:MessageForwarder#
已測試的 MessageForwarder class 管理郵件列表,有一個 getFromAddress 方法。
新需求:支援匿名郵件列表,匿名列表的 “from” 地址應該基於 domain 而非原始發件者。
步驟一:寫失敗的測試
public void testAnonymous () throws Exception {
MessageForwarder forwarder = new AnonymousMessageForwarder();
forwarder.forwardMessage(makeFakeMessage());
assertEquals("anon-members0" + forwarder.getDomain(),
expectedMessage.getFrom()[0].toString());
}步驟二:建立子類別
// AnonymousMessageForwarder 繼承 MessageForwarder
protected InternetAddress getFromAddress(Message message)
throws MessagingException {
String anonymousAddress = "anon-" + listAddress;
return new InternetAddress(anonymousAddress);
}這很快就讓測試通過了。

Figure 8.1: Subclassing MessageForwarder
潛在問題:設計退化#
如果持續使用繼承來新增功能,會遇到問題。例如又需要一個 OffListMessageForwarder,如果需要同時具備匿名和 off-list 兩個功能,就無法用單一繼承解決。

Figure 8.2: Subclassing for two differences
解決方案:及時重構
可以停下來,將匿名轉發改為透過 configuration property 實現:
Properties configuration = new Properties();
configuration.setProperty("anonymous", "true");
MessageForwarder forwarder = new MessageForwarder(configuration);然後修改 getFromAddress 方法來處理配置:

Figure 8.3: Delegating to MailingConfiguration
private InternetAddress getFromAddress(Message message)
throws MessagingException {
String fromAddress = getDefaultFrom();
if (configuration.getProperty("anonymous").equals("true")) {
from = getAnonymousFrom();
}
else {
from = getFrom(Message);
}
return new InternetAddress(from);
}進一步可以將配置邏輯移到專門的 MailingConfiguration class,最終重新命名為 MailingList。

Figure 8.4: Moving behavior to MailingConfiguration

Figure 8.5: Moving more behavior to MailingConfiguration

Figure 8.6: MailingConfiguration renamed as MailingList
Liskov Substitution Principle (LSP)#
使用 Programming by Difference 時要注意 LSP 違規:
Liskov Substitution Principle 要求子類別的物件應該能替代父類別的物件而不產生錯誤。當我們覆寫具體方法時,可能改變了使用該 class 的程式碼的語意。
避免 LSP 問題的經驗法則:
- 盡可能避免覆寫具體方法
- 如果必須覆寫,看看是否能在覆寫方法中呼叫被覆寫的方法
Normalized Hierarchy(正規化層級)#
理想的類別層級應該是 normalized 的:沒有任何 class 的方法覆寫了從父類別繼承的具體方法。每個方法要嘛是 abstract 且由子類別實作,要嘛就只存在於那個 class 中。

Figure 8.7: Normalized hierarchy
總結#
- TDD 讓你為任何能放入測試的程式碼新增功能,且能安全地移除重複
- Programming by Difference 讓你透過繼承快速引入變化,再利用測試來重構到更乾淨的設計
- 測試讓這些轉變變得非常快速和安全