有時你連把那個巨大類別放進測試框架的力氣都沒有,卻又非改不可。Feathers 的務實解法:不要在無測試的舊碼裡動刀,改成把新行為寫成全新、有測試的一塊,再從舊碼呼叫它。這就是 Sprout(長一段新碼出來)與 Wrap(包一層新碼上去)——它們在新舊責任之間畫一條清楚的界線,讓你今天就能開始累積測試。
🧠 為什麼需要它
遺留碼的兩難是「Pay now or pay later」:先把舊類別鋪好測試最乾淨,但你可能沒時間、依賴也深到建不起來。Sprout / Wrap 讓你繞過「先測舊碼」這一步,把新增的部分寫成獨立、可測的碼。
WARNING
這四招讓你加入「經過測試的新碼」,但它們不會測到呼叫它的舊碼。它們是起點,不是終點——目的是先讓球滾起來,日後有人受夠繞路,自然會回頭把大類別拉進測試。
⚖️ 四種手法
Sprout=「長出」一段新碼;Wrap=「包住」既有呼叫。各有 method / class 兩個層級。
Sprout Method — 把新邏輯長成一個獨立方法
需求:post 之前先過濾重複的 entries。別把新邏輯攪進舊迴圈——那樣新舊碼糊在一起、又沒法單獨測。改成抽一個新方法,用 TDD 開發它,舊方法只是去呼叫它:
class TransactionGate {
// ← 全新、可獨立測試的方法(用 TDD 寫出來)
uniqueEntries(entries) {
return entries.filter((e) => !this.bundle.listManager.hasEntry(e));
}
postEntries(entries) {
const toAdd = this.uniqueEntries(entries); // ← 舊碼只多一行呼叫
toAdd.forEach((e) => e.postDate());
this.bundle.listManager.add(toAdd);
}
}- 優點:新舊碼界線清楚、新碼獨立可測、受影響的變數好追。
- 缺點:放棄了順手改善舊方法的機會;舊方法多一個「孤立的呼叫」,讀者可能困惑為何在此。
Sprout Class — 連 Sprout Method 都塞不進去時,開一個新類別
當現有類別根本無法放進測試框架(建構就拉進整個系統),連在裡面加方法都不划算,就把新職責放進一個全新類別:
// 舊的巨大 QuarterlyReportGenerator 完全不進測試框架;
// 新表頭邏輯獨立成一個可測的小類別
class QuarterlyReportTableHeader {
make() {
return "<tr><td>Department</td><td>Manager</td><td>Profit</td></tr>";
}
}- 兩種觸發情境:① 本來就該是獨立新職責;② 被舊類別的不可測性逼出來。
- 優點:比直接改舊碼更有信心、更安全。
- 缺點:增加概念複雜度——新人可能不懂為何冒出這些小類別。
Wrap Method — 在既有方法「前後」加一步,不改它本體
想在 pay() 發薪時順便記帳。直接在 pay() 裡加 logPayment() 會製造時間耦合(把不相干的事綁在一起只因要同時發生)。改法:把原方法改名藏起來,新方法同名同簽名,串起「記帳+原邏輯」:
class Employee {
pay() {
this.logPayment(); // 新行為
this.dispatchPayment(); // ← 原本的 pay(),改名收進 private
}
dispatchPayment() { /* …原本 pay() 的內容,一字未改… */ }
logPayment() { /* 新的、可測的行為 */ }
}對呼叫端完全透明。優點:不撐大既有方法、新舊行為明確分離。缺點:新功能只能在舊功能「之前或之後」跑,無法穿插;命名有時被迫將就。
Wrap Class — 用一層新類別包住舊類別(= Decorator)
當有多個呼叫端都要透明地加同一個行為,就上升到類別層級:新類別持有舊物件、實作相同介面,在委派前後插入新行為。這正是 Decorator 模式。
class LoggingEmployee {
constructor(employee) {
this.employee = employee; // 包住原物件
}
pay() {
this.logPayment(); // 加上的行為
this.employee.pay(); // 委派回原本的
}
logPayment() { /* … */ }
}- 何時用:新行為完全獨立、不想用低層雜訊污染舊類別;或舊類別已大到受不了,包一層當未來拆分的起點。
- 多呼叫端時可先用 Extract Interface 抽出共用介面,讓 wrapper 與原類別可互換。
🌱 vs. 🎁 怎麼選
| 情境 | 手法 |
|---|---|
| 舊方法演算法清楚,新增的是一個明確步驟 | Sprout Method |
| 新功能與舊功能同等重要,想串成高階流程 | Wrap Method |
| 現有類別無法放進測試框架 | Sprout / Wrap Class |
| 多個呼叫端要透明地加同一行為 | Wrap Class (Decorator) |
TIP
Sprout / Wrap 都用 TDD 開發那塊新碼——新碼一出生就有測試。它們不是最優雅的設計,但讓你在不敢碰舊碼的當下,仍能安全前進。
🔑 Takeaways
- 不動無測試的舊碼:把新行為寫成獨立、有測試的一塊,再從舊碼呼叫——這是 Sprout / Wrap 的共同精神。
- Sprout(長新碼):能加方法就 Sprout Method,連方法都塞不進就 Sprout Class。
- Wrap(包舊碼):改名藏起原方法即 Wrap Method;跨多呼叫端就用 Wrap Class=Decorator。
- 直接在舊方法裡塞新邏輯會製造時間耦合;Wrap 用「前後包」避開它。
- 這是起點不是終點:它不測舊碼,只是先累積測試、劃清界線,日後再回頭把大類別拉進 測試。
No notes yet — jot your takeaways or Q&A here.