保證一個類別全程只有一個實例,並提供一個全域存取點。聽起來方便,但它同時偷偷把「全域狀態」和「隱藏依賴」帶進系統,多數情況其實該用 DI 注入單一實例來取代。
🧠 When to Use
TIP
訊號:某資源天生就只能有一份(硬體裝置、作業系統層級的鎖),而且讓多個實例存在會直接出錯——不是「只是懶得傳參數」。
- 真正物理上唯一的資源(如某個 I/O 裝置、單一進程鎖)。
- 需要一個全域協調點,且確定不需要在測試裡替換它。
- 即使如此,先想清楚:你要的是「唯一性」,還是只是「方便全域拿到」?後者該用 DI。
⚖️ Structure & Variants
Kotlin 版 — object 一行搞定
object Config { // 編譯器保證執行緒安全的延遲初始化、全程唯一實例
val maxConn = 10
fun reload() { /* ... */ }
}
Config.maxConn // 全域存取點- Kotlin 的
object直接就是 Singleton,省去手寫 double-checked locking。但「容易寫」不代表「該用」。
Java 經典版 — 為什麼這麼囉嗦
- 傳統得處理執行緒安全(double-checked locking +
volatile)或用enum/ holder idiom,才能保證唯一又不出 race。 - 這份歷史包袱正提醒:Singleton 的「唯一性保證」其實不便宜,而且和測試替身天生衝突。
更好的解 — DI 注入單一實例
- 把物件交給 依賴注入 容器以 singleton scope 管理:依然是一個實例,但依賴顯式注入、測試時可換 mock。
- 你得到「全程一份」的好處,卻不用付「全域存取點 + 隱藏依賴」的代價。這幾乎總是優於手寫 Singleton。
⚠️ Misuse & Anti-patterns
- 隱藏依賴:類別內部偷偷
Config.x,從簽章完全看不出它依賴什麼,耦合被藏起來、難以推理與替換。 - 難以測試:全域狀態跨測試殘留,測試之間互相污染;又無法在不改原碼下注入假物件——這是它最被詬病之處。
- 常被當成全域變數的遮羞布:多數 Singleton 其實只是「懶得傳參數」,本質是可變全域狀態。真有唯一性需求,也該用 DI 注入單一實例,而非寫死的 Singleton。
🔑 Takeaways
- 它保證「全程唯一 + 全域存取」,但代價是隱藏依賴與全域可變狀態,常被視為反模式。
- Kotlin
object讓它一行就寫出來,但「好寫」不等於「該用」。 - 多數情況的正解:用 DI 以 singleton scope 注入單一實例——保留唯一性,去掉全域存取點的毒性。
No notes yet — jot your takeaways or Q&A here.