Singleton
December 25, 2021由於語言特性取捨的關係,有些設計的方式,在某些語言很常見,然而另一個語言不見得會出現類似的模式;另一方面,有些設計,可以通用於語言之間,然而有時同一個概念,卻是因語言不同,在實現上會有很大的差異。
例如,若某種資源,在某個需求下只需要一個,例如,代表應用程式的 application 物件、代表全域的 global 物件、代表環境的 context 物件等,這類資源稱為 singleton。
singleton 與其說是模式,不如說是需求,因為不同的語言環境,實現 singleton 的方式可能迥異。有些語言在語法上能阻止客戶端直接建構資源;某些語言在某些需求下,提供內建機制,不用任何設計,取得的某項資源,本身就是 singleton…
有的語言只能建立慣例,或只能以文件規範,透過某個介面來取得資源,以實現 singleton 的需求,不過,事實上這或許才是 singleton 的出發點,建立一個介面,客戶端透過介面來取得資源,之後你要怎麼實現 singleton 的取得,客戶端是一無所知的。
Java 的實現
Java 的 java.lang.Runtime
實例,代表程式執行環境,每個應用程式只需要一個實例,可以透過 static
的 getRuntime()
方法取得,例如:
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.iterator
、Symbol.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,在只有一個類別載入器,以及有多個類別載入器的情況下,實現時的考量與方式會有所不同。