單例模式 (Singleton pattern)
什麼是單例模式
在類別裡應用單例模式 (Singleton pattern),可以讓使用者取得該類別的一個實例(Instance),並且無論何時何處取得,得到的都會是同一個實例。
實例指的是類別產生的物件,每次使用 new關鍵字產生的都會是新的實例,過程稱為實例化。他們並不相同,因為這些實例指向不同的記憶體位置。
為什麼要有單例模式
什麼狀況下會需要使用單例模式呢?
- 需要限制物件產生的數量時。
- 需要在專案各處取得同一個物件時。
- 需要跨專案重複利用該類別時。
因為這種都是取得同一個物件實例的特性,讓單例模式適合用在資料庫連線(Database connection)這種場合,因為我們使用資料庫系統時,若使用的是多個不同的實例,可能會使讀寫相互影響,產生競爭條件(Race condition)。
單例模式帶來的另一個好處是,我們容易在專案的任何地方用上這個物件實例,若是類別封裝的夠完整,也很容易分離出來,達到跨專案重用。適合用在日誌系統(Logger)這種較為封閉的系統。
如何使用單例模式
要使用單例模式的話,需確認使用的程式語言是否具備:
- 靜態(static)類別與屬性。
- 可自定義為私有(private)的建構子。
我們使用的TypeScript剛好滿足以上兩點,可以開始實作單例模式。
我們利用私有建構子讓實例只能從類別內部生成,而無法從外界生成新實例。接著使用公開的靜態方法讓外界可以取得類別內的唯一實例,這樣就能確保我們無論何時何地都是取得到同一個類別實例。
接下來驗證取得的是否為同一個實例,這裡使用了嚴格相等(===)來對取得的實例做判斷。
我們另外創建一個非單例的一般類別來比較兩者的差別:
這樣我們就完成了單例模式的實作了。這裡提供另一種實作單例模式的方法,利用TypeScript的命名空間(namespace)來做到:
使用上跟class的用法大同小異,一樣做一些測試:
這種作法的原因是TypeScript在舊版本(2.0之前),沒有私有建構子,只能利用命名空間來限制實例化。現在TypeScript已經支援私有建構子,因此建議使用class的方式,邏輯上會較接近於其他語言。
講完優點,接下來要講一下缺點。
以下幾點需要特別注意:
- 轉譯過後的JavaScript是沒有private建構子的,因此若需要使用到轉譯後的JavaScript程式碼,可能會造成該類別意外被實例化。
- 因為TypeScript及JavaScript是單執行緒(Single thread)的程式語言,在本篇文章中沒有提到一個細節,像是以多執行緒語言來實作單例模式,需要用同步鎖(像是Java中的Synchronized)來避免高併發的狀況下造成單例模式失效。
- 有些人認為單例模式是反模式,因為單例模式實際上違反了SRP原則。沒有做好規劃時,可能導致過度使用的狀況發生,造成系統修改與擴充上的困難。若是僅有少數地方需使用該單例物件,可考慮使用之前提過的依賴注入(Dependency Injection)來減少系統對單例物件的引用。
總結一下
單例模式語法簡單、易於使用、效果強大,但卻具有龐大的副作用,很容易使系統的耦合度不降反升,因此如何設計一個好的單例類別才是最重要的。
單例類別盡量同時擁有三個條件:
- 控制資源的訪問通道,避免競爭條件。
- 系統多處都需要訪問該單例類別。
- 單例類別不該相依於其他類別。
遵循以上三條規則,能夠打造出好的單例類別,若是無法遵守,那還是盡可能不要使用單例模式,以免造成後續維護上的困難。
如果對於我的文章或程式碼有任何問題,歡迎在下方留言指教。
若有幫助到你,也歡迎給文章拍手一下,讓我在寫文章的路上更加進步!