一旦你把類別放進了測試,加功能就不必再靠 Sprout / Wrap 繞道了——可以正面下手。Feathers 給兩把主力工具:對能測的碼用 TDD 一次專注一件事;想快速試新行為時用 以差異編程(programming by difference)——先靠繼承覆寫出差異,再用測試護體重構成乾淨設計。重點在「先快、再正」:繼承讓你一步到位,測試讓你敢把它整理掉。
🧠 為什麼:Sprout/Wrap 是繞道,這裡是正面走
Sprout 和 Wrap 讓你在沒有測試時加碼,但它們留下痕跡:不改善舊碼、可能製造重複。
IMPORTANT
只要能把類別放進測試,就該用本頁的手法正面面對。Sprout/Wrap 留在碼裡的痕跡,是「我們當時無法做得更好」的小小提醒——能測了,就別再繞。
⚖️ 遺留碼的 TDD 迴圈:多一個「第 0 步」
一般 TDD 是:① 寫失敗測試 → ② 讓它編譯 → ③ 讓它通過 → ④ 移除重複 → ⑤ 重複(見 紅-綠-重構)。用在遺留碼上,前面多一步:
第 0 步:先把你要改的類別放進測試框架。 然後才進入紅-綠-重構——而且「綠」那步有個額外紀律:盡量不要改動既有程式碼,只加新的。
TDD 最大的價值在遺留碼裡尤其明顯:它逼你一次只做一件事——你不是同時寫功能又重構,而是分開做。這正是敢在舊碼上動手的關鍵。
⚖️ 以差異編程:先覆寫出差異,再重構掉它
新需求若只是既有行為的一個「變體」,可以先用子類別覆寫快速做出來:
① 先靠繼承做出差異(快速通過測試)
// 已測試的 MessageForwarder 有 getFromAddress()。
// 新需求:匿名列表的寄件地址要不同 → 子類別覆寫那一個方法
class AnonymousMessageForwarder extends MessageForwarder {
getFromAddress(message) {
return new InternetAddress("anon-" + this.listAddress);
}
}一個子類別、覆寫一個方法,測試很快變綠。這就是以差異編程。
② 察覺設計退化:兩個維度的差異就爆炸
再來一個 OffListMessageForwarder。若某天要「同時匿名 + off-list」,單一繼承組合不出來——繼承維度一多就爆炸。這是停下來重構的訊號。
③ 用測試護體,重構成組態驅動
// 把「差異」從繼承維度移到資料維度
const forwarder = new MessageForwarder({ anonymous: true });
getFromAddress(message) {
const from = this.config.anonymous ? this.getAnonymousFrom()
: this.getFrom(message);
return new InternetAddress(from);
}再把這些組態邏輯搬進專責物件(MailingConfiguration → 最終正名為 MailingList)。繼承是快速起手,測試是讓你敢把它重構掉的網。
WARNING
當心 LSP(里氏替換): 覆寫「具體方法」可能悄悄改掉依賴父類別的程式碼語意。經驗法則:① 盡量別覆寫具體方法;② 非覆寫不可時,看能否在覆寫裡呼叫被覆寫的版本。
TIP
目標是 normalized hierarchy(正規化層級):沒有任何類別覆寫繼承來的具體方法——每個方法要嘛是抽象、由子類別實作,要嘛只屬於它自己那層。這也正是 Template Method 的精神:把變異點做成抽象掛鉤,而不是覆寫實體。
🔑 Takeaways
- 能把類別放進測試,就用 TDD 正面加功能,別再靠 Sprout/Wrap 繞道。
- 遺留碼 TDD 多一個第 0 步:先讓類別可測;綠燈階段盡量只加不改既有碼。
- 以差異編程=先靠子類別覆寫快速做出變體,再用測試護體重構成組態/委派的乾淨設計。
- 繼承維度一多就會爆炸——那是重構的訊號;避免覆寫具體方法、追求 normalized hierarchy。
- 重構時靠的仍是 提取函式 這類手法 + 測試這張網。
No notes yet — jot your takeaways or Q&A here.