四種看似合理、實則破壞鬆耦合的 DI 反模式:Control Freak、Service Locator、Ambient Context、Constrained Construction。它們的共同病灶是同一個——把依賴藏起來,讓建構子不再誠實宣告「我需要什麼」,於是編譯期錯誤被推遲到執行期。
🧠 When to Use
TIP
訊號:一個類別的建構子空空如也,你卻在方法內部看到它「變出」了資料庫、時鐘、logger——依賴被藏在 new、Locator.get() 或某個 .Current 靜態存取後面。這頁教你辨識這四種藏法。
- 檢視既有程式:判斷它是真的在做 DI,還是只是「看起來像」。
- Code review 時辨識偽裝成解法的耦合。
- 理解「為什麼建構子注入是預設」——這四種都是它的反例。
⚖️ Structure & Variants
Control Freak —— 在 Composition Root 以外自己 new 會變的依賴
// 反模式:業務類別自己建立具體依賴,焊死了實作
class ProductService {
private val repository = SqlProductRepository() // 緊耦合,測試換不掉
}- 最常見的一種:直接
new、工廠內硬編具體型別、或多載建構子在無參版本裡塞預設依賴。 new本身不是罪:對 stable 依賴(String、List、DTO)new完全正常;只有在 Composition Root 外對會變的依賴(DB、網路、時鐘)new才是臭味。- 修正:改建構子注入,把組裝挪到 Composition Root。
Service Locator —— 最危險,因為它偽裝成解法
// 反模式:主動向全域容器「要」依賴
class ProductService {
fun featured() {
val repo = Locator.getService<ProductRepository>() // 依賴被藏起來了
}
}- 三宗罪:隱藏依賴(建構子不再誠實)、編譯期錯誤推遲到執行期(漏註冊要 runtime 才爆)、把容器依賴拖進整個 codebase(人人都得認得 Locator)。
- 書中判準一針見血:DI 容器在 Composition Root 以外被使用,就是 Service Locator。 即使是「官方」容器,用錯地方仍是反模式。
- 修正:改建構子注入,讓依賴在建構子明白宣告。
Ambient Context —— 用 static accessor 提供環境式依賴
- 典型:
DateTime.Now、靜態Log.info(...)、Thread.CurrentPrincipal這種「隨手可取」的全域存取。 - 問題和 Service Locator 同源:隱藏依賴、難測(得操作全域狀態,平行測試互相干擾)、違反 SRP(人人可取、鼓勵職責擴散)、共享可變狀態(多執行緒競態)。
- 修正:把靜態存取換成建構子注入(例如注入
Clock而非直接讀系統時間)。
Constrained Construction —— 假設所有實作都有固定建構子簽章
// 反模式:假設 plugin 都有無參數建構子,好用反射晚綁
val type = Class.forName(typeName)
val plugin = type.getDeclaredConstructor().newInstance() as Plugin- 為了用反射做 late binding,硬性要求實作類別具備特定(通常是無參)建構子——於是這些實作自己沒辦法再注入依賴。
- 這是一種隱式介面合約,編譯器無法檢查。
- 修正:改用 DI 容器或在 Composition Root 以建構子注入建立,不再限制簽章。
⚠️ Misuse & Anti-patterns
- 把 Service Locator 當 DI 賣:因為都「動態取得依賴」就以為等價——差別正在「誠不誠實宣告」。
- 用工廠洗白 Control Freak:工廠內部仍硬編具體型別,只是把
new換了地方,耦合沒解。 - 靜態 logger/時鐘視為理所當然:它們是最不被察覺的 Ambient Context,卻讓時間相關邏輯難以測試。
🔑 Takeaways
- 四種反模式同一病根:藏依賴,讓建構子說謊、把編譯期錯誤推到執行期。
- Control Freak=在 Composition Root 外
new會變的依賴;Service Locator=主動向容器索取(在 Root 外用容器即是它);Ambient Context=static accessor 環境式依賴;Constrained Construction=假設固定建構子簽章。 - 四者的修正方向幾乎都是同一句:改建構子注入、把組裝收回 Composition Root。
No notes yet — jot your takeaways or Q&A here.