設計模式:單例模式 (Singleton)
9 分鐘
Singleton 是最簡單、也最常被濫用的設計模式之一。它的目標只有一個:確保一個類別在整個應用程式中只會有一個實例,並提供一個全域的存取點。
意圖與用途
Singleton 適用於這類情境:
- 全域配置物件 (應用程式的不同部分都要讀同一份設定)
- 日誌記錄器 (所有模組共用同一個 Logger,把記錄集中在一起)
- 資料庫連線池 (建立連線成本高,共用同一個 Pool 才划算)
- 快取 (整個應用共用一份需要跨模組存取的資料)
共同的特徵是:這些資源建立有成本,或者狀態本來就需要共用,重複建立對應用程式沒有好處。
實作方式
TypeScript 中最直接的實作:
TypeScript
class Singleton {
private static instance: Singleton;
// 私有建構 —— 止外部直接 new
private constructor() {}
static getInstance(): Singleton {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
doSomething(): void {
console.log('Singleton is working');
}
}
const a = Singleton.getInstance();
const b = Singleton.getInstance();
console.log(a === b); // true把建構子標記為 private 是關鍵,這樣外部程式碼就無法直接呼叫 new Singleton(),唯一的入口是 `getInstance() —— 在第一次被呼叫時才建立實例,之後都回傳同一份快取下來的實例。
實務範例:結構化日誌系統
TypeScript
type LogLevel = 'info' | 'warn' | 'error';
class Logger {
private static instance: Logger;
private logs: string[] = [];
private constructor(private prefix: string = '[App]') {}
static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
log(level: LogLevel, message: string): void {
const entry = `${this.prefix} [${level.toUpperCase()}] ${new Date().toISOString()} - ${message}`;
this.logs.push(entry);
console.log(entry);
}
getLogs(): string[] {
return [...this.logs];
}
}
// 不論在哪個模組呼叫 getInstance(),拿到的都是同一個 Logger
const logger1 = Logger.getInstance();
const logger2 = Logger.getInstance();
logger1.log('info', 'Application started');
logger2.log('warn', 'Memory usage high');
console.log(logger1.getLogs()); // 包含上面兩條記錄不論在應用程式的哪個模組取得 Logger,都是同一個實例,所有日誌集中在同一個地方,不會分散。
使用時機
適用時機
- 整個應用程式確實只需要一份資源
- 共用狀態是真的有必要,而不是為了方便才這樣用
- 實例的「全域性」本身就是你要的特性,而非副作用
常見陷阱
- 測試困難:共用狀態是單元測試最大的麻煩。測試 A 殘留的狀態會滲入測試 B,導致測試不穩定、結果還跟執行順序有關。
- 隱性的全域狀態:模組默默依賴同一個實例,這層依賴是看不見的,難以追蹤。
- 貪圖方便而濫用:很多看似需要單例的情況,其實用依賴注入就能解決,而且能讓依賴關係保持明確。
實務原則:先考慮依賴注入;只有在真的需要共用實例語意時,才動用 Singleton。
總結
Singleton 解決的問題很明確:消除重複建立資源的浪費,並提供一個一致的全域存取點。
它的缺點,恰恰就是它吸引人的地 —— 個全域實例。全域狀態會讓各部分之間的依賴變得隱性,也讓測試變得脆弱。
用對地方,它高效又乾淨;當成「我到處都要用這個」的捷徑來用,它就會變成你半年後得花好幾個小時除錯的隱患。