遺留碼難測,是因為它天生糾纏:想建一個物件,卻連帶拉進半個系統。Feathers 的解法不是重寫,而是換一種眼光看程式——不再是「一長串指令」,而是一個佈滿可替換點的結構。這些點就叫接縫(seam):一個能改變行為、卻不必動到該處原始碼的地方。找到接縫,你就找到了把舊碼拉進測試的入口。
🧠 什麼是接縫(Seam)
接縫是程式裡一個「可以改變行為、但不需要修改該處原始碼」的地方。 這是全書最核心的概念。
每個接縫都配一個啟用點(Enabling Point)——你在那裡做決定,選擇要用哪一種行為。沒有啟用點的「可替換」只是空談;接縫的價值就在於:你能在測試時,從啟用點切換到一個受控的替身,從而把討厭的依賴排除、把內部行為感測出來。
IMPORTANT
接縫的思維,是去看程式碼裡已經存在的替換機會,而不是先大改再測。面對遺留碼,關鍵問題從「怎麼重寫」變成「哪裡有接縫可以下手」。
⚖️ 三種接縫
從「程式文字 → 可執行程式」的每個階段,都會冒出不同類型的接縫。
① 物件接縫 (Object Seam) — 最常用、最有用
在 OO 語言裡,多型本身就是接縫:把寫死的協作者換成一個可注入的介面,啟用點就是「建立物件、決定傳誰進去」的地方。
// 前 — 依賴寫死在裡面,沒有接縫:Sale 直接抓全域螢幕
class Sale {
scan(barcode) {
const item = catalog.lookup(barcode);
ScreenDriver.showLine(`${item.name} ${item.price}`); // 無法在測試中攔截
}
}
// 後 — 引入介面,建立「物件接縫」
class Sale {
constructor(display) {
this.display = display; // ← 啟用點:由外部決定傳哪個 display
}
scan(barcode) {
const item = catalog.lookup(barcode);
this.display.showLine(`${item.name} ${item.price}`);
}
}測試時從啟用點傳入一個受控替身,就能感測 Sale 到底送了什麼字上螢幕:
const fake = { last: "", showLine(l) { this.last = l; } };
const sale = new Sale(fake); // ← 在啟用點選擇替身
sale.scan("1");
expect(fake.last).toBe("Milk $3.99");- 接縫:
display.showLine(...)的呼叫點 - 啟用點:建立
Sale時傳入哪個display
(替身的 stub / mock 分野見 測試替身。)
② 連結接縫 (Link Seam) — 動建構設定,不動原始碼
編譯不是最後一步:連結器/類別載入把呼叫解析成真正的實作。你不改一行呼叫端,只換「解析到誰」——啟用點在建構腳本或 classpath。
正式建構: classpath = [ real/db.jar , app.jar ] → 呼叫打到真實 DB
測試建構: classpath = [ fake/db.jar , app.jar ] → 同樣的呼叫打到假 DB- 接縫:對函式庫的呼叫點(原始碼完全不動)
- 啟用點:classpath / 連結設定
- 代價:替換發生在「看不見的地方」,務必讓「測試用的是替身」清晰可查,否則除錯會很痛。
③ 預處理接縫 (Preprocessing Seam) — C/C++ 的額外武器
在 C/C++,巨集預處理器在編譯前先跑,給了一層文字替換的接縫。
#ifdef TESTING
#define db_update(acct, item) { last_item = (item); last_acct = (acct); }
#endif- 接縫:
db_update(...)的呼叫點 - 啟用點:是否定義
TESTING - 警告:正式碼裡濫用條件編譯會嚴重傷可讀性、藏 bug;只當「打破測試依賴」的最後手段。
TIP
三種接縫的共同心法:先找到「行為在哪裡被決定」,把那個決定點變成你能在測試裡撥動的開關。 物件接縫最乾淨,優先用它;連結/預處理接縫是語言不給你多型時的備案。
🔗 接縫是「感測」與「分離」的入口
Feathers 說打破依賴只有兩個理由:感測(sensing)——看不到內部計算結果,就打破依賴把它讀出來;分離(separation)——根本塞不進測試框架,就打破依賴把它拆出來。接縫正是執行這兩件事的著力點:在接縫處換上替身,你要嘛能讀到結果,要嘛能讓那段碼獨立跑起來。
🔑 Takeaways
- 接縫 = 不改該處原始碼就能換行為的地方;每個接縫都要有一個啟用點當開關。
- 三型:物件接縫(多型/注入,最常用)、連結接縫(建構/classpath)、預處理接縫(C/C++ 巨集)。
- 用接縫的眼光看遺留碼,問題從「怎麼重寫」變成「哪裡能下手鋪網」。
- 接縫是感測與分離的入口,讓你能為糾纏的舊碼寫 特徵測試。
- 物件接縫的替身細節 → 測試替身;當你連接縫都懶得找、只想加新行為,改用 Sprout / Wrap。
No notes yet — jot your takeaways or Q&A here.