Singleton Pattern(單例模式)
是什麼?
Singleton Pattern 確保一個類別只有一個實例存在,並提供一個全域的存取點讓所有人都能取得這個實例。它透過將建構子設為私有,並由類別自己管理唯一實例的生命週期。
ℹ️GoF 分類
Singleton 屬於建立型模式(Creational Pattern),重點在於控制物件的建立過程,確保實例的唯一性。
什麼時候用?
用以下 if/then 條件判斷:
- 如果整個應用程式確實只能有一份該資源(例如設定檔管理器、連線池),且多份實例會導致資料不一致或資源浪費 → 用 Singleton
- 如果物件的建立成本極高(需要讀取大量設定、建立網路連線),且整個應用生命週期只需要一份 → 用 Singleton
- 如果你只是想要「方便存取」而非「嚴格唯一」→ 用 DI 容器的 Singleton Scope 而非手寫 Singleton
常見應用場景
| 場景 | 原因 | |------|------| | 設定檔管理器 | 整個應用只需讀取一次設定,多份實例浪費 I/O | | 連線池管理 | 連線數必須集中控制,多份池會超出連線上限 | | 日誌記錄器 | 集中寫入避免 race condition 和檔案鎖定衝突 | | 快取管理器 | 多份快取會造成資料不一致 |
什麼時候不該用?
⚠️過度設計警告
Singleton 常被濫用為「全域變數的包裝」。判斷標準:如果移除唯一性限制、改用普通的 DI 注入也能正常運作 → 你不需要 Singleton,改用 DI 容器的 Singleton Scope(如 .NET 的 services.AddSingleton)。手寫 Singleton 會讓單元測試變困難,因為它引入了隱藏的全域狀態(Hidden Global State,指物件的依賴關係無法從建構子看出來,而是藏在 static 存取中)。
執行流程
私有建構子
將 constructor 設為 private,防止外部直接 new
靜態實例欄位
類別內部持有自己的唯一實例
提供存取方法
透過靜態方法(如 GetInstance)回傳唯一實例
延遲初始化
第一次被呼叫時才建立實例(Lazy Initialization)
全域共用
所有呼叫者取得的都是同一個實例
流程解讀:Singleton 的核心機制是「控制建立入口」。Private 建構子封死了外部 new 的路,靜態欄位確保全域只有一份參考,而延遲初始化(Lazy Initialization,指第一次使用時才建立物件而非程式啟動時就建立)既節省資源又避免了啟動時的不必要開銷。在多執行緒環境中,延遲初始化必須搭配執行緒安全機制(如 C# 的 Lazy<T>、Java 的 Double-Checked Locking)才能確保只建立一次。
程式碼範例
C# 版本
// 執行緒安全的 Singleton(使用 Lazy<T>)
public sealed class AppConfig
{
private static readonly Lazy<AppConfig> _instance =
new Lazy<AppConfig>(() => new AppConfig());
public static AppConfig Instance => _instance.Value;
public string DatabaseConnection { get; private set; }
public int MaxRetryCount { get; private set; }
private AppConfig()
{
// 模擬從設定檔讀取
DatabaseConnection = "Server=localhost;Database=MyApp";
MaxRetryCount = 3;
}
}
// 使用
var config1 = AppConfig.Instance;
var config2 = AppConfig.Instance;
Console.WriteLine(config1 == config2); // True — 同一個實例
Console.WriteLine(config1.DatabaseConnection);TypeScript 版本
class AppConfig {
private static instance: AppConfig;
public readonly databaseConnection: string;
public readonly maxRetryCount: number;
private constructor() {
this.databaseConnection = "Server=localhost;Database=MyApp";
this.maxRetryCount = 3;
}
static getInstance(): AppConfig {
if (!AppConfig.instance) {
AppConfig.instance = new AppConfig();
}
return AppConfig.instance;
}
}
// 使用
const config1 = AppConfig.getInstance();
const config2 = AppConfig.getInstance();
console.log(config1 === config2); // true
console.log(config1.databaseConnection);Python 版本
class AppConfig:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self):
if self._initialized:
return
self.database_connection = "Server=localhost;Database=MyApp"
self.max_retry_count = 3
self._initialized = True
# 使用
config1 = AppConfig()
config2 = AppConfig()
print(config1 is config2) # True — 同一個實例
print(config1.database_connection)Java 版本
public final class AppConfig {
private static volatile AppConfig instance;
private final String databaseConnection;
private final int maxRetryCount;
private AppConfig() {
this.databaseConnection = "Server=localhost;Database=MyApp";
this.maxRetryCount = 3;
}
public static AppConfig getInstance() {
if (instance == null) {
synchronized (AppConfig.class) {
if (instance == null) {
instance = new AppConfig();
}
}
}
return instance;
}
public String getDatabaseConnection() { return databaseConnection; }
public int getMaxRetryCount() { return maxRetryCount; }
}
// 使用
AppConfig config1 = AppConfig.getInstance();
AppConfig config2 = AppConfig.getInstance();
System.out.println(config1 == config2); // true
System.out.println(config1.getDatabaseConnection());結構圖
結構解讀:Client 透過 getInstance() 靜態方法存取 Singleton,永遠不會直接碰到建構子。Singleton 類別內部管理唯一的 static instance,第一次呼叫時建立、後續呼叫直接回傳。這確保了不管多少個 Client 呼叫,拿到的都是同一個物件參考。
實戰補充
💡資深開發者經驗
執行緒安全:在多執行緒環境,務必確保 Singleton 初始化是安全的。C# 用 Lazy<T>、Java 用 Double-Checked Locking 或 enum。
DI 容器優先:現代開發中,與其自己寫 Singleton,不如在 DI 容器中註冊為 Singleton Scope(如 .NET 的 services.AddSingleton<T>())。這樣既保有單一實例的好處,又能輕鬆替換為 mock 做單元測試。
避免 Singleton 地獄:如果你的專案到處都是 Singleton,這通常是架構問題的信號。Singleton 應該只用於真正需要全域唯一的資源。
理解測驗
🤔 Singleton Pattern 如何防止外部建立多個實例?
🤔 在 C# 中,以下哪種方式能確保 Singleton 的執行緒安全?
🤔 為什麼現代開發建議用 DI 容器的 Singleton Scope 取代手寫 Singleton?
面試常見問題
Q: Singleton 為什麼會讓單元測試變困難?如何解決?
A: Singleton 透過靜態方法存取,無法在測試中替換為 mock。解法有二:(1) 改用 DI 容器的 Singleton Scope,透過介面注入依賴,測試時可以替換;(2) 如果必須手寫 Singleton,提供 Reset() 方法(僅限測試環境)或用 interface 包裝存取。
Q: 多執行緒環境下,Singleton 初始化可能出什麼問題?
A: 兩個執行緒同時檢查 instance 為 null,可能各自建立一個實例,破壞唯一性。解法因語言而異:C# 用 Lazy<T>(內建執行緒安全);Java 用 Double-Checked Locking(volatile + synchronized)或 enum;Python 因 GIL 較少出問題,但仍建議用 threading.Lock。
Q: Singleton 和靜態類別(Static Class)有什麼差異?
A: Singleton 是物件,可以實作介面、被繼承、支援多型,也能透過 DI 注入。靜態類別不是物件,無法實作介面,無法作為參數傳遞。當你需要可替換性(如測試時 mock)→ 用 Singleton;當操作純粹是工具方法且不需要狀態 → 用靜態類別。
相關模式
| 模式 | 關係 | |------|------| | Factory Method / Abstract Factory | 工廠本身常實作為 Singleton,確保全域只有一個工廠入口 | | Facade | Facade 有時也會設計為 Singleton,作為子系統的唯一入口 | | Flyweight | Flyweight Factory 通常是 Singleton,集中管理共享物件池 | | State / Strategy | State 和 Strategy 的具體實作如果是無狀態的(Stateless),可以用 Singleton 避免重複建立 |
重點整理
💡一句話記住
Singleton Pattern = 全域唯一、集中管理、私有建構。 口訣:「Private 建構堵死門,Static 方法開唯一窗」
| 概念 | 說明 | |------|------| | Singleton(單例類別) | 管理自己的唯一實例,提供全域存取點 | | Private Constructor | 防止外部直接建立新實例 | | Static Instance | 類別內部持有的唯一實例 | | 核心好處 | 全域唯一 + 延遲初始化 + 集中管理 | | 代價 | 隱藏的全域狀態、難以單元測試、可能造成緊耦合 |