單例模式 (Singleton pattern)

創建並取得類別唯一的實例

Huashen
Apr 19, 2021

什麼是單例模式

在類別裡應用單例模式 (Singleton pattern),可以讓使用者取得該類別的一個實例(Instance),並且無論何時何處取得,得到的都會是同一個實例

實例指的是類別產生的物件,每次使用 new關鍵字產生的都會是新的實例,過程稱為實例化。他們並不相同,因為這些實例指向不同的記憶體位置

為什麼要有單例模式

什麼狀況下會需要使用單例模式呢?

  1. 需要限制物件產生的數量時。
  2. 需要在專案各處取得同一個物件時。
  3. 需要跨專案重複利用該類別時。

因為這種都是取得同一個物件實例的特性,讓單例模式適合用在資料庫連線(Database connection)這種場合,因為我們使用資料庫系統時,若使用的是多個不同的實例,可能會使讀寫相互影響,產生競爭條件(Race condition)。

單例模式帶來的另一個好處是,我們容易在專案的任何地方用上這個物件實例,若是類別封裝的夠完整,也很容易分離出來,達到跨專案重用。適合用在日誌系統(Logger)這種較為封閉的系統。

如何使用單例模式

要使用單例模式的話,需確認使用的程式語言是否具備:

  1. 靜態(static)類別與屬性。
  2. 可自定義為私有(private)的建構子。

我們使用的TypeScript剛好滿足以上兩點,可以開始實作單例模式。

我們利用私有建構子讓實例只能從類別內部生成,而無法從外界生成新實例。接著使用公開的靜態方法讓外界可以取得類別內的唯一實例,這樣就能確保我們無論何時何地都是取得到同一個類別實例。

接下來驗證取得的是否為同一個實例,這裡使用了嚴格相等(===)來對取得的實例做判斷。

比較之下得到true,代表兩個物件是相同的實例。

我們另外創建一個非單例的一般類別來比較兩者的差別:

比較之下得到false,代表兩個物件是不同的實例。

這樣我們就完成了單例模式的實作了。這裡提供另一種實作單例模式的方法,利用TypeScript的命名空間(namespace)來做到:

使用上跟class的用法大同小異,一樣做一些測試:

用命名空間(namespace)一樣可以實作單例模式。

這種作法的原因是TypeScript在舊版本(2.0之前),沒有私有建構子,只能利用命名空間來限制實例化。現在TypeScript已經支援私有建構子,因此建議使用class的方式,邏輯上會較接近於其他語言。

講完優點,接下來要講一下缺點。

以下幾點需要特別注意:

  1. 轉譯過後的JavaScript是沒有private建構子的,因此若需要使用到轉譯後的JavaScript程式碼,可能會造成該類別意外被實例化
  2. 因為TypeScript及JavaScript是單執行緒(Single thread)的程式語言,在本篇文章中沒有提到一個細節,像是以多執行緒語言來實作單例模式,需要用同步鎖(像是Java中的Synchronized)來避免高併發的狀況下造成單例模式失效。
  3. 有些人認為單例模式是反模式,因為單例模式實際上違反了SRP原則。沒有做好規劃時,可能導致過度使用的狀況發生,造成系統修改與擴充上的困難。若是僅有少數地方需使用該單例物件,可考慮使用之前提過的依賴注入(Dependency Injection)來減少系統對單例物件的引用。

總結一下

單例模式語法簡單、易於使用、效果強大,但卻具有龐大的副作用,很容易使系統的耦合度不降反升,因此如何設計一個好的單例類別才是最重要的。

單例類別盡量同時擁有三個條件:

  1. 控制資源的訪問通道,避免競爭條件
  2. 系統多處都需要訪問該單例類別。
  3. 單例類別不該相依於其他類別

遵循以上三條規則,能夠打造出好的單例類別,若是無法遵守,那還是盡可能不要使用單例模式,以免造成後續維護上的困難。

如果對於我的文章或程式碼有任何問題,歡迎在下方留言指教。

若有幫助到你,也歡迎給文章拍手一下,讓我在寫文章的路上更加進步!

--

--

Huashen

嗨,我是Huashen,一位軟體工程師,這裡會記錄我的程式設計心得與筆記。