Composer:統一的組裝者概念#
本章首先引入 Composer 這個統一術語,泛指任何負責組裝 Dependencies 的程式碼。無論是 Composition Root 中的手動組裝、DI Container 的自動解析,還是其他負責建立物件圖的機制,都屬於 Composer 的範疇。
Composer 不僅決定如何組裝物件圖,也決定何時建立和何時釋放每個 Dependency——這就是 Object Lifetime 管理的核心。
三種生命週期模式 (Lifestyle)#
Singleton#
一個實例,整個應用程式共用。
- 在應用程式啟動時建立,結束時銷毀
- 最適合無狀態 (Stateless) 或執行緒安全 (Thread-safe) 的服務
- 效能最佳,因為只需建立一次
- 需特別注意共用狀態與並行存取問題

Figure 8.3: 將同一個 Dependency 實例注入多個消費者以重複使用
// Singleton: 在 Composition Root 中只建立一次
var repository = new SqlProductRepository(connectionString);
// 所有需要 IProductRepository 的地方都使用這個實例Transient#
每次請求都建立一個新實例。
- 最簡單、最安全的選擇
- 不需要擔心狀態共用或執行緒安全問題
- 缺點是可能造成較多的記憶體配置與 GC 壓力
- 當不確定該選哪種 Lifestyle 時,Transient 是最安全的預設選擇

Figure 8.2: 組裝多個獨立的 Dependency 實例
Scoped#
在一個定義好的範圍內共用一個實例。
- 典型的 Scope 是一次 Web Request
- 非常適合 Unit of Work 模式(例如同一個 Request 共用同一個 DbContext)
- Scope 結束時,該範圍內的實例一併釋放

Figure 8.9: Scoped Lifestyle 表示每個指定 Scope 最多建立一個實例
Lifestyle 的選擇原則:預設用 Transient,需要跨請求共用狀態時用 Scoped,確定無狀態且執行緒安全時才考慮 Singleton。
Pure DI 中的生命週期管理#
使用 Pure DI 時,生命週期完全由 Composer 透過程式碼的結構來控制:
- Singleton:在 Composition Root 的最外層建立實例,傳入所有需要它的地方
- Transient:每次組裝物件圖時都
new一個新的 - Scoped:在 Scope 的進入點(如每個 Web Request 的開始)建立實例,在同一 Scope 內共用
// Pure DI 中的 Lifestyle 控制
var singleton = new CachingRepository(connectionString); // Singleton
void HandleRequest()
{
var scoped = new DbContext(connectionString); // Scoped: 每個 Request 一個
var transient = new ProductService(scoped, singleton); // Transient: 每次都新建
}Disposable Dependencies 的管理#
核心原則:消費者絕不負責釋放#
消費者 (Consumer) 永遠不應該 Dispose 被注入的 Dependencies。 這是 Object Lifetime 管理中最重要的原則之一。
原因很明確:
- 消費者不知道這個 Dependency 是否被其他地方共用
- 消費者不知道這個 Dependency 的 Lifestyle(可能是 Singleton)
- Dispose 的責任屬於建立者,也就是 Composer

Figure 8.5: 釋放 Dependencies 的事件順序
Composer 負責釋放#
// Composer 建立,Composer 負責 Dispose
using var repository = new SqlProductRepository(connectionString);
var service = new ProductService(repository);
service.Execute();
// repository 在這裡被正確釋放錯誤的 Lifestyle 配置#
Captive Dependency(被俘虜的依賴)#
最危險的 Lifestyle 錯誤:Singleton 持有 Scoped 或 Transient 的 Dependency。
// 危險!Singleton 持有了 Scoped 的 DbContext
var context = new DbContext(connectionString); // 應該是 Scoped
var singleton = new CachingService(context); // Singleton 持有了它問題:
- Scoped/Transient 的 Dependency 被 Singleton 俘虜,無法正常釋放
- 導致過期資料 (Stale Data)——Scoped 物件的狀態跨越了原本的 Scope
- 引發並行存取 (Concurrency) 問題——非執行緒安全的物件被多執行緒共用
Captive Dependency 是一種隱性的 Bug,通常不會立即出錯,而是在高負載或長時間運行後才浮現,非常難以除錯。
Leaky Abstraction#
當消費者的設計被 Dependency 的 Lifestyle 影響時,就產生了 Leaky Abstraction。
- 消費者不應該知道或關心 Dependency 的生命週期
- 如果消費者因為 Dependency 是 Singleton 而省略某些保護措施,就洩漏了實作細節
Thread-based Lifetime#
以 Thread 為基礎的生命週期管理(如 ThreadLocal<T>)看似合理,實則脆弱:
- 在 async/await 的環境中,程式碼可能在不同 Thread 上恢復執行
- 導致 Scoped 物件意外跨越 Thread 或在錯誤的 Thread 上存取
- 應使用 Scope-based(而非 Thread-based)的生命週期管理

Figure 8.12: Thread-specific Dependencies 在非同步環境中可能導致並行錯誤
Lifestyle 選擇決策表
| 條件 | 建議 Lifestyle |
|---|---|
| 無狀態、執行緒安全 | Singleton |
| 有狀態但不確定 | Transient |
| Unit of Work / 每次請求共用 | Scoped |
| 建立成本高且無狀態 | Singleton |
實作了 IDisposable | 通常 Scoped 或 Transient,由 Composer 管理釋放 |