Singleton Pattern(單例模式)

是什麼?

Singleton Pattern 確保一個類別只有一個實例存在,並提供一個全域的存取點讓所有人都能取得這個實例。它透過將建構子設為私有,並由類別自己管理唯一實例的生命週期。

ℹ️GoF 分類

Singleton 屬於建立型模式(Creational Pattern),重點在於控制物件的建立過程,確保實例的唯一性。

什麼時候用?

用以下 if/then 條件判斷:

常見應用場景

| 場景 | 原因 | |------|------| | 設定檔管理器 | 整個應用只需讀取一次設定,多份實例浪費 I/O | | 連線池管理 | 連線數必須集中控制,多份池會超出連線上限 | | 日誌記錄器 | 集中寫入避免 race condition 和檔案鎖定衝突 | | 快取管理器 | 多份快取會造成資料不一致 |

什麼時候不該用?

⚠️過度設計警告

Singleton 常被濫用為「全域變數的包裝」。判斷標準:如果移除唯一性限制、改用普通的 DI 注入也能正常運作 → 你不需要 Singleton,改用 DI 容器的 Singleton Scope(如 .NET 的 services.AddSingleton)。手寫 Singleton 會讓單元測試變困難,因為它引入了隱藏的全域狀態(Hidden Global State,指物件的依賴關係無法從建構子看出來,而是藏在 static 存取中)。

執行流程

1

私有建構子

將 constructor 設為 private,防止外部直接 new

2

靜態實例欄位

類別內部持有自己的唯一實例

3

提供存取方法

透過靜態方法(如 GetInstance)回傳唯一實例

4

延遲初始化

第一次被呼叫時才建立實例(Lazy Initialization)

5

全域共用

所有呼叫者取得的都是同一個實例

流程解讀:Singleton 的核心機制是「控制建立入口」。Private 建構子封死了外部 new 的路,靜態欄位確保全域只有一份參考,而延遲初始化(Lazy Initialization,指第一次使用時才建立物件而非程式啟動時就建立)既節省資源又避免了啟動時的不必要開銷。在多執行緒環境中,延遲初始化必須搭配執行緒安全機制(如 C# 的 Lazy<T>、Java 的 Double-Checked Locking)才能確保只建立一次。

程式碼範例

C# 版本

csharp
// 執行緒安全的 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 版本

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 版本

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 版本

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 (AppConfig)
creates / returns
static instance

結構解讀: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&lt;T&gt;(內建執行緒安全);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 | 類別內部持有的唯一實例 | | 核心好處 | 全域唯一 + 延遲初始化 + 集中管理 | | 代價 | 隱藏的全域狀態、難以單元測試、可能造成緊耦合 |

你可能也想看

Strategy PatternFactory Method Pattern

按 ← → 鍵切換課程