Singleton模式通常被認為是比較容易理解和運用的設計模式。目前,網上已經有相當多的資料講解Singleton的基礎知識,本文試圖避免重複性的介紹,而是嘗試從不同的角度更深入地探討Singleton。
“保證對象有唯一的執行個體,並且提供一個全域訪問點”是Singleton模式比較常見的描述。不知您是否意識到,這個定義本身就散發著某種"bad smell"。為什麼要把“保證對象有唯一執行個體”的建立職責,和“提供一個全域訪問點”的訪問職責混入一個模式呢?
為了更清楚地說明這個問題,我們假設有一個Singleton的類A,並通過靜態方法A.Instance()提供唯一執行個體。使用者類B通常會直接以A.Intance().Do()的方式來使用它(因為這樣很方便)。這裡我們需要追問的是B的設計,“B一定要A的單例對象才行嗎?不是單例可以嗎?”。其實,絕大多數情況B所需要的僅僅是A的對象,它甚至根本不需要知道“單例”為何物。但不幸的是以方便的理由,“單例”成功地混入了B,這就造成B對A的“單例耦合”。"單例耦合"直接損害了B的可測試性,因為我們無法對類A進行mock,只能帶著A測試B,擴大了測試邊界。一般來講,具有好的可測試性的模組,通常表現為依賴注入的開放式設計,因為這樣才方便通過mock等方式類比外部依賴,讓測試專註於模組本身的邏輯,即測試邊界只包含被測模組本身。
通過上面的分析,問題已經很清楚了,B是A的使用者,它只關注A提供的功能介面;A只能有唯一執行個體是A的建立者的事情,與B無關。所以,我們提倡B應該採用開放式的依賴注入,比如通過建構函式或者通過Setter讓外部注入A的執行個體,不要僅僅因為方便而濫用Singleton。對於建立唯一執行個體的職責可以採用傳統Singleton模式,也可以採用其它方式。從理論上講,建立A的職責不一定要放在A內部來實現,我們完全可以結合原廠模式,設計所謂的單例工廠,保證從這個工廠出品的對象是唯一執行個體。
其實,某些情況下,我們的真實需求並非“保證類有唯一對象”而是“保證類對象有唯一的狀態”。這裡涉及兩個問題:
第一“既然是類,又是唯一,為什麼不乾脆弄成static算了,還要Singleton模式幹嘛?”。其實,這是為了避免使用者B對類A的直接依賴,採用static就無法把B設計成依賴注入式的,採用static意味著失去了多態。這裡的多態特別指對Singleton類本身有介面要求,那麼static設計就直接被排除在外了。
第二“既然使用者不關心是一個對象還是多個對象,是否可能類有多個對象,但都共用同一狀態,用這種方式實現狀態唯一性需求?”。答案是肯定的,MonoState模式正是採用這種方式,把唯一狀態通過static成員封裝在類內部,讓所有對象共用同一狀態。與Singleton相比,MonoState更好地分離了狀態唯一性和對象使用,避免出現耦合。
更多關於Singleton模式的探討,可以參考:
1. Patterns I Hate #1: Singleton
2. Why Singletons are Evil