cover

Singleton 大概是所有設計模式裡最好懂的一個,但也是最容易被用錯的一個。

先講結論

Singleton 就是「整個程式只准有一個實例」。聽起來很美好對吧?但它本質上就是一個全域變數的包裝紙,用錯了會讓你的程式碼變成一團義大利麵。該用的時候用、不該用的時候別硬用——這才是重點。

什麼時候你真的需要 Singleton?

你有沒有遇過這種狀況:兩個模組各自 new 了一個 ConfigManager,結果 A 改了設定、B 完全不知道?或是 DB connection 被重複建立了十幾條,伺服器直接跪了?

這些場景的共通點是:資源必須唯一、狀態必須一致

classDiagram
    class Singleton {
        -static instance : Singleton
        -_config : Object
        -Singleton()
        +static getInstance() Singleton
        +getConfig(key) any
        +setConfig(key, value) void
    }

最經典的用法:Config Manager

全域設定這東西,你絕對不想讓它有兩份。一份就夠了,多了只會打架。

class ConfigManager {
    constructor() {
        this._config = {};
    }
 
    static getInstance() {
        if (!ConfigManager.instance) {
            ConfigManager.instance = new ConfigManager();
        }
        return ConfigManager.instance;
    }
 
    getConfig(key) {
        return this._config[key];
    }
 
    setConfig(key, value) {
        this._config[key] = value;
    }
}
 
const config = ConfigManager.getInstance();
config.setConfig('API_URL', 'https://api.example.com');
console.log(config.getConfig('API_URL')); // https://api.example.com

進階一點:用 IIFE 做模組級 Singleton

如果你覺得上面那種寫法太 Java 味了,JavaScript 其實可以用閉包做得更乾淨。重點是把 instance 藏起來,外面根本摸不到。

const DatabaseConnection = (function() {
    let instance;
 
    function createInstance() {
        let connection = null;
 
        function connect() {
            connection = { id: Math.random() };
            console.log('建立新的資料庫連線');
        }
 
        return {
            getConnection: function() {
                if (!connection) connect();
                return connection;
            }
        };
    }
 
    return {
        getInstance: function() {
            if (!instance) instance = createInstance();
            return instance;
        }
    };
})();
 
const conn1 = DatabaseConnection.getInstance().getConnection();
const conn2 = DatabaseConnection.getInstance().getConnection();
console.log(conn1 === conn2); // true — 同一條連線

那 Singleton 有什麼問題?

老實說,問題不少:

  • 測試超痛苦:全域狀態意味著測試之間會互相污染。你的 test A 改了 config,test B 就爆了,還查不出原因
  • 隱性依賴:程式碼裡到處都 getInstance(),但你看不出誰依賴誰。重構的時候就知道痛了
  • 違反單一職責:一個 class 同時管「業務邏輯」和「確保自己只有一個」,這兩件事根本不相干

所以很多現代框架(Angular、NestJS)選擇用 DI(依賴注入) 來管理 singleton scope,而不是讓你自己寫 getInstance()。框架幫你保證只有一個,你專心寫業務邏輯就好。

什麼時候該考慮替代方案?

反問自己:「這個東西真的需要全域唯一嗎?還是我只是懶得傳參數?」

如果答案是後者——用 DI。如果答案是前者——確認你的 Singleton 是 thread-safe 的(在 Node.js 裡不太需要擔心,但 Java 就得用 double-checked locking)。


Singleton 是設計模式的 Hello World,但千萬別因為它簡單就到處亂用——你不會想在 code review 被問「為什麼這個也要 Singleton」的。


延伸閱讀