Singleton

December 25, 2021

由於語言特性取捨的關係,有些設計的方式,在某些語言很常見,然而另一個語言不見得會出現類似的模式;另一方面,有些設計,可以通用於語言之間,然而有時同一個概念,卻是因語言不同,在實現上會有很大的差異。

例如,若某種資源,在某個需求下只需要一個,例如,代表應用程式的 application 物件、代表全域的 global 物件、代表環境的 context 物件等,這類資源稱為 singleton。

singleton 與其說是模式,不如說是需求,因為不同的語言環境,實現 singleton 的方式可能迥異。有些語言在語法上能阻止客戶端直接建構資源;某些語言在某些需求下,提供內建機制,不用任何設計,取得的某項資源,本身就是 singleton…

有的語言只能建立慣例,或只能以文件規範,透過某個介面來取得資源,以實現 singleton 的需求,不過,事實上這或許才是 singleton 的出發點,建立一個介面,客戶端透過介面來取得資源,之後你要怎麼實現 singleton 的取得,客戶端是一無所知的。

Java 的實現

Java 的 java.lang.Runtime 實例,代表程式執行環境,每個應用程式只需要一個實例,可以透過 staticgetRuntime() 方法取得,例如:

var runtime = Runtime.getRuntime();

Runtime 實現 singleton 的方式極為簡單:

public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    public static Runtime getRuntime() {
        return currentRuntime;
    }

   /** Don't let anyone else instantiate this class */
   private Runtime() {}

   ...
}

將建構式設為 private,客戶端就不能 new,就這麼簡單?是的!這不是〈Simple Factory〉嗎?是啊!Simple Factory 確實是 Java 實現 singleton 的方式之一,之前說過了,模式名稱只是個便於溝通的工具,不是什麼有你就沒有我的教條,這邊的重點是如何創建 singleton,只要能達成目的,用什麼模式都無所謂。

另一方面,與其說用什麼方式實作都無所謂,不如說你決定讓客戶端如何取得 singleton 才是重點!假設有個 Singleton 必須是獨一無二,一開始只是簡單的設計:

public class Singleton {
    private static Singleton currentSingleton = new Singleton();

    public static Singleton getSingleton() {
        return currentSingleton;
    }

   private Singleton() {}

   ...
}

客戶端使用 Singleton.getSingleton() 來取得唯一實例,就這麼過了一段日子,後來你決定,真的需要 Singleton 實例時再建立好了,也就是想實現延遲初始(lazy initialization):

public class Singleton {
    private static Singleton instance;

    public static Singleton getInstance() {
        if(instance == null) {
            instance = new Singleton();
        }

        return instance;
    }

    private Singleton() {}
    ...
}

這對客戶端沒差,畢竟客戶端還是以 Singleton.getSingleton() 的方式來取得 Singleton 實例,後來有一天,客戶端必須在多執行緒環境呼叫 Singleton.getSingleton(),你發現以上的實作,無法保證一定只有一個 Singleton,因為可能會有以下的情況:

Thread1: if(instance == null)        // true
Thread2: if(instance == null)        // true

Thread1: instance = new Singleton(); // 產生一個實例
Thread2: instance = new Singleton(); // 又產生一個實例

Thread1: return instance;            // 回傳一個實例
Thread2: return instance;            // 又回傳一個實例

為了避免資源同時競爭而產生多個實例的情況,加上同步(synchronized)機制:

public class Singleton {
    private static Singleton instance = null;

    synchronized static public Singleton getInstance() {
        if(instance == null) {
            instance = new Singleton();
        }
        return instance;
    }

	private Singleton(){}
	...
}

雖然解決了問題,不過若是執行緒多到競爭情況頻繁,會造成相當的效能低落…你想了想…double check 可以改善:

public class Singleton {
    private static Singleton instance = null;

    public static Singleton getInstance() {
        if(instance == null){
            synchronized(Singleton.class){
                if(instance == null) {
                     instance = new Singleton();
                }
            }
        }
        return instance;
    }

	private Singleton(){}
	...
}

然後日後….因此…總之,singleton 該怎麼實現,環境的影響很大,因此 singleton 的重點在於,固定取得單一資源的介面,這麼一來,客戶端就不會受日後實現的影響,畢竟客戶端只在乎取得的要是 singleton 就可以了,實現細節是另一件事。

因此 singleton 的原則,就是那老調牙的原則,也就是關注分離(separation of concerns)。

JavaScript 的 Symbol

有些語言針對某些需求,語言本身就提供了 singleton 的實現,例如 JavaScript 的 Symbol,本身就有許多 singleton 資源,像是 Symbol.iteratorSymbol.toPrimitive 等,目的是作為獨一無二的協定符號。

JavaScript 開發者可以隨時建立新的 Symbol,例如 Symbol()Symbol('Protocol.iterable') 就建立了新的 Symbol;然而,如果自訂的 Symbol 具有全域的概念,想要實現 singleton,可以透過 Symbol.for 建立,例如 Symbol.for('Protocol.iterator')

Symbol.for 會採用指定的字串作為依據,如果字串沒有對應的符號存在,就會建立新符號並存入註冊表,若有的話就會傳回已建立的符號。

Symbol.for 實現了單例註冊表(Registry of Singleton)的概念,事實上,JavaScript 沒有阻止開發到處建立新 Symbol,只不過如果想實現 singleton,開發者自己去使用 Symbol.for,因為它提供了單一介面來取得 Symbol 資源。

Python 的實現

怎麼實現 singleton,開發者得自己決定,重點在於決定後,客戶端就要這麼用,別輕易變更 singleton 的取得介面。

其他語言想實現像 JavaScript 的 Symbol.for 並不是什麼難事,只要有字典之類的資料結構就可以了,像是 Java 的 Map,或者是 Python 的 dict

例如,來看看 Python 實現單例註冊表:

class SingletonRegistry:
    __registry = {}
    
    def __init__(self):
        raise Singleton.__single
        
    def getInstance(classname):
        if classname in SingletonRegistry.__registry:
            return SingletonRegistry.__registry[classname]
        singleton = getattr(sys.modules[__name__] , classname)()
        SingletonRegistry.__registry[classname] = singleton
        return singleton

這個簡單的例子,利用了 Python 的 Introspection 機制,可以取得各種類型的 singleton,Java 想做類似實現的話,可以透過 Reflection API。

當然,還是要看你的需求,不同環境或需求下,可能會有不同的考量,例如,在 Java 中透過 Reflection API 實現 singleton,在只有一個類別載入器,以及有多個類別載入器的情況下,實現時的考量與方式會有所不同。