遺留碼最常見的阻塞是:想建一個物件來測,卻連帶拉進資料庫、網路、vendor 物件、一整串建構鏈。Feathers 用整本書的第三部分收了 24 種打破依賴的手法,共同目的只有一個——在盡量不改變行為的前提下,鑿出一個 接縫,把糾纏的類別放進測試框架。這頁不逐條背誦,而是把它們整理成一張可查的地圖:先認清你卡在哪,再挑對應的招。
🧠 一條共同原則:先可測,再談優雅
這些手法有個反直覺的共通點——它們常常會「暫時」讓設計變差(多一個 setter、暴露一個 static、破壞一點封裝)。這是刻意的取捨:
IMPORTANT
打破依賴的第一目標是讓程式碼可測,而不是讓它變漂亮。先鑿出接縫、鋪上測試網;有了網,之後的結構改善才安全。許多手法(Expose Static Method、Supersede Instance Variable、Primitivize Parameter…)都只是中間步驟,不是終點。
還有一條貫穿全書的安全守則:動這些手法時盡量 Preserve Signatures(保留簽名)、Lean on the Compiler(讓編譯器幫你找出所有要改的地方),把手動出錯的機會壓到最低。
⚖️ 按「你卡在哪」挑手法
卡點 A — constructor / 方法內 new 出難搞的物件
物件在建構或方法內被 new 出來,你無法從外面換掉它。最乾淨的順序:
- Parameterize Constructor / Parameterize Method:把
new出來的東西改成參數傳入(就是 dependency injection)。舊 constructor 委派給新 constructor 並保留原本的new,client 完全不受影響。
class MailChecker {
constructor(receiver = new MailReceiver(), periodSec) {
this.receiver = receiver; // 測試傳 fake,正式走預設
this.periodSec = periodSec;
}
}- Extract and Override Factory Method:把 constructor 裡的建立抽成一個 factory method,測試子類別 override 它回傳 fake。(C++ 注意:constructor 內呼叫 virtual 不會 dispatch 到子類別——那就改用 Extract and Override Getter 的 lazy 版。)
- Supersede Instance Variable:建構有重要副作用、無法參數化時的最後手段——加一個 setter 在建構後替換。缺點:容易忘了呼叫而測到真物件。
卡點 B — 想 mock 一個具體類別(沒有介面)
- Extract Interface(最常用):抽出你測試真正需要的那幾個方法成介面,讓原類別實作它,測試用 fake 實作同一介面。命名別只加
I前綴——想清楚這組方法的本質概念,好名字本身就在改善設計。 - Extract Implementer:當該類別被到處直接引用、不想改一堆型別宣告時——把實作搬到
XxxImpl,讓原名變成介面。 - Subclass and Override Method(全書最核心之一):直接子類別化,override 掉會惹麻煩的方法,塞進測試要的行為。幾乎能和其他所有手法搭配。
class TestingMessageForwarder extends MessageForwarder {
sendMessage(msg) { this.sent.push(msg); } // 不真的送,只記錄
}- Extract and Override Call:只想換掉某一個外部呼叫時,把它抽成方法,測試子類別 override。
卡點 C — 全域變數 / Singleton / static 方法擋路
static 依賴無法用物件接縫替換(Feathers 稱為 static cling),要先「去 static 化」:
- Introduce Instance Delegator:在同類別加一個 instance method 委派給 static,呼叫端改用實例版——這樣就能用物件接縫替換了。
- Replace Global Reference with Getter:把對全域/Singleton 的直接引用改成可 override 的 getter。
- Encapsulate Global References:把一組相關全域包進一個類別,之後整組可替換。
- Introduce Static Setter:給 Singleton 加一個 static setter 讓測試注入 fake(會弱化封裝,記得在
setUp/tearDown清理共享狀態、留註解說明是為測試)。
卡點 D — 方法/類別大到、或依賴深到根本進不了測試框架
- Break Out Method Object:把一個怪物方法整個搬進新類別,參數變 constructor 參數、區域變數變 instance 變數——長方法瞬間可測、可 sensing。
- Pull Up Feature / Push Down Dependency:把「乾淨的方法群」上移到抽象父類別去測(Pull Up);或把「髒依賴」下推到子類別、父類別留抽象(Push Down)。
- Primitivize Parameter:依賴鏈太深時的牆角手法——用原始型別寫一個 free function 先做出功能,類別再委派過去。會留下未測、複製資料的難看狀態,是 Sprout Class 的前身,只在你確定會回頭整理時才用。
卡點 E — 語言限定的接縫(C / C++ / 動態語言)
- C/C++:Link Substitution(連結時換 dummy library)、Replace Function with Function Pointer、Definition Completion、Template Redefinition(用 template 參數換型別)。
- 動態語言(JS/Ruby/Python):Text Redefinition——執行期直接重新定義方法,打破依賴最快;代價是失去編譯器的型別安全網,要靠測試補回。
- Adapt Parameter:參數型別是 vendor/framework 給的、你無法 Extract Interface 時,包一層自訂介面 + adapter,測試給 fake。
WARNING
這些手法多半會先弱化設計(多 setter、暴露 static、破壞封裝)。那是為了先取得可測性這個更大的價值。留一行註解說明「這是為測試而開的縫」,並把它當成待還的技術債,而不是新常態。
🔑 Takeaways
- 24 種手法,一個目的:在不改行為下鑿出 接縫,把糾纏的類別放進測試。
- 先分辨卡點再挑招:new 物件 → Parameterize / Extract-Override Factory;想 mock → Extract Interface / Subclass and Override Method;static/全域 → Introduce Instance Delegator;太大進不去 → Break Out Method Object。
- 可測 > 優雅:很多手法是刻意的中間步驟,先鋪網再改善。
- 用 Preserve Signatures 與 Lean on the Compiler 降低手動出錯。
- 鑿出縫後,接著就是為它寫 特徵測試、掛上 測試替身。
No notes yet — jot your takeaways or Q&A here.