負責組裝的一方(Composer——不論是 Composition Root 的手工組裝,還是容器)不只決定「怎麼組」,還決定每個依賴何時建立、何時釋放、被幾個人共用。這就是 Lifestyle:Singleton、Transient、Scoped 三選一,選錯會埋下難查的並行與過期資料 bug。
🧠 When to Use
TIP
訊號:兩個請求的資料互相污染、或某個服務在高負載下才偶發詭異狀態——很可能是某個依賴的 Lifestyle 配錯了,被跨請求或跨執行緒共用了不該共用的狀態。
- 決定一個依賴要「全程共用一個」還是「每次都給新的」。
- 依賴握有可變狀態(連線、快取、
DbContext),必須想清楚共用範圍。 - 依賴實作了
Closeable/Disposable,得決定由誰、在何時釋放。
⚖️ Structure & Variants
三種 Lifestyle
- Singleton:一個實例、全應用程式共用。啟動時建、結束時銷毀。效能最好,但只適合無狀態或執行緒安全的服務,要小心共用狀態與並行。
- Transient:每次要用都給一個新的。最簡單、最安全,不必擔心共用與執行緒;代價是較多配置與 GC 壓力。不確定選哪個時,Transient 是最安全的預設。
- Scoped:在一個定義好的範圍內共用一個(典型是「一次 Web request」)。最適合 Unit of Work(同一 request 共用同一個
DbContext);範圍結束時一併釋放。
Pure DI 下用『程式碼結構』控制 Lifestyle
// Singleton:在最外層建一次,傳給所有需要它的人
val cache = CachingRepository(connectionString)
fun handleRequest() {
val dbContext = DbContext(connectionString) // Scoped:每個 request 一個
val service = ProductService(dbContext, cache) // Transient:每次都新建
service.execute()
}- 不需要容器也能管生命週期——建立的位置就決定了共用範圍:最外層建=Singleton、每次進 request 建=Scoped、每次組裝就
new=Transient。
釋放原則:消費者絕不 Dispose 被注入的依賴
- 消費者不知道這個依賴是否被別人共用、也不知道它的 Lifestyle(可能是 Singleton)——擅自釋放會殃及他人。
- 釋放責任屬於建立者(Composer):誰建立、誰負責
use {}/close()。
// Composer 建立,Composer 負責釋放
SqlProductRepository(connectionString).use { repo ->
ProductService(repo).execute()
} // repo 在此正確釋放;ProductService 從不碰釋放⚠️ Misuse & Anti-patterns
- Captive Dependency(被俘虜的依賴):最危險的錯配——Singleton 持有了 Scoped/Transient 依賴。被俘的物件無法在原範圍結束時釋放,造成過期資料與並行問題。它通常不當場出錯,而在長時間運行後才浮現,極難除錯。
- Leaky Abstraction:消費者因為「知道」某依賴是 Singleton 而省略保護措施——生命週期細節洩漏到了消費端。消費者不該知道也不該關心依賴的 Lifestyle。
- Thread-based lifetime:用
ThreadLocal綁生命週期,在 async/await 下程式可能換執行緒恢復,導致 Scoped 物件跑錯 Thread。用 Scope-based,不要用 Thread-based。 - 無腦全 Singleton:為省配置把有狀態物件設成 Singleton,是並行 bug 的溫床。
🔑 Takeaways
- 三種 Lifestyle:Singleton(全域共用、須無狀態/執行緒安全)、Transient(每次新建、最安全預設)、Scoped(範圍內共用,適合 Unit of Work)。
- Pure DI 下,建立的位置就是 Lifestyle:最外層=Singleton、每 request=Scoped、每次組裝=Transient。
- 釋放是建立者的責任,消費者絕不 Dispose 被注入的依賴。
- 最毒的錯配是 Captive Dependency(Singleton 俘虜短命依賴)——過期資料+並行,且拖很久才爆。
No notes yet — jot your takeaways or Q&A here.