Singleton Pattern 介紹,是否為 Anti-pattern?

Camel
嗨,世界
Published in
9 min readDec 7, 2020

--

Photo by Halacious on Unsplash

你是否曾幾何時遇到一個情境,在一個應用程式中,希望有一個始終如一的物件,它扮演著單一入口,你可以很容易地呼叫他、創建它與使用它,它自始自終只有一個,沒有第二個它。

  • 你不用擔心多個實體造成互相衝突
  • 你不用擔心多個實體複雜的運作流

沒錯,他就是 Singleton Pattern !

謎之音:聽起來好像很方便但又哪裡怪怪的….?

謎之音:宣告 Global Variable就好啦,哪次不 Global (大誤)

在本篇中,我們將介紹 Singleton Pattern 的基本實作與概念,以下的範例皆假設於 single thread 環境中,關於在各程式語言的優化與 thread safe 等等的實作,在此篇中較無深入的討論。

除此之外,大家在 Google “Singleton Pattern” 時可能有注意到不少文章篇幅是在描述關於以下論述

Why is Singleton considered an anti-pattern…?

Is singleton an Anti-Pattern …?

Singleton Anti-Pattern

在最後的篇幅中,我們將對以上的部分提出我的看法及觀點,對於 Singleton Pattern 已有初步認識的讀者們,建議可以直接跳到此段落,也許會有意想不到的收穫。

介紹

首先看看 Singleton Pattern 的定義:

Ensure a class only has one instance, and provide a global point of access to it.

以定義來看,我們可以歸類為以下兩點

  • 在程式生命週期中創建多次,只會建立一個 instance
  • 有一個 global point 可以存取到這個 instance 的接口

以 Singleton Pattern 較為適用的情境,可以舉例如下圖

https://aishwaryavaishno.files.wordpress.com/2013/05/singleton.png
  • 有一個儲存的 Music Store 的類別,負責儲存所有類型的 music
  • 透過分類的 Melodies , RockDevotional 的類別可以存取 Music Store
  • 不管在哪個分類的類別存取到的 Music Store 都是相同的單一實體

使用 Singleton 在這個場景中,簡化了操作對象為 Music Store 單一出入口,不論是 Music Store 的建立或運作都顯得更為簡單.

架構

Class diagram exemplifying the singleton pattern.

在程式架構上,透過改寫類別的 constructor 確保單一實體,概念如下:

首先檢查是否已存在 instance

  • 無: 新建一個 singleton instance
  • 有: 回傳既有的 singleton instance

接下來提供一個外部存取 instance 的接口:

  • 提供 new 或 public 方法 getInstance 取得以上的 instance

實作

接下來我們依照上述的架構,使用 JavaScript 實作 Singleton Music Store

首先我們先建立一個簡單的 MusicStore.js

接下來試著處理 new MusicStore() 呼叫多次,應該回傳相同的 instance

因此改寫 MusicStore.js 的 constructor

經過對 constructor 的改寫,符合了 singleton 的構成要件

  • new MusicStore() 多次只會建立一個實體
  • 提供一個 new MusicStore() 接口存取單一實體

接下來,我們試著建立一隻 index.js 驗證以上兩點

index.js 中我們嘗試 new MusicStore() 多次拿到單一實體,並且驗證每次拿到的實體是相同的。

更進一步,我們可以試著實作 getInstance 與如何撰寫對應的 test case,礙於篇幅,就不在此詳述。

如果有興趣進一步參閱完整實作的讀者可以參考此 github

接下來帶大家看看 Singleton Pattern 為人所詬病及爭議的點

Singleton Pattern is an Anti-pattern?

  1. Singleton Pattern 會提高耦合性與測試上的困難

這也是絕大多數提出 anti-pattern 的主要論點,像是以下這兩篇文章

我的觀點是應該拆分為以下兩點來進一步分析

  • Singleton Pattern 帶來高耦合性
    這部分其實跟類別的實作有相當大的關係,並非 Singleton Pattern 的原罪
    甚至可以適時的應用 Dependency Injection pattern 改善
  • Singleton Pattern 帶來了 Global State 導致測試上的困難
    這部分我是認同的,畢竟單一實體就是一種 Global State,不論是否使用 Singleton Pattern,我們都應盡可能的減少 Global State 的產生與依賴。

其實以上這兩點也有不少的文章做了進一步的說明,像是以下此篇文章

2. Singleton Pattern 違反 SRP 原則?

They violate the single responsibility principle: by virtue of the fact that they control their own creation and lifecycle.

上面這段節錄自 Stackoverflow 的問題,描述為 Singelton Pattern 負責了物件的生命週期/建造及本身的職責,因此違反 SRP 原則

首先看看 SRP 的定義:A class should have only one reason to change.”

可以理解為一個類別所做的多項職責是否有高度依賴性,若有的話,應該盡可能進一步的分離。

再來我們看看此問題所描述的違反論點:

Singleton Pattern 類別負責了以下兩種職責

  1. 類別控制物件生命週期
  2. 類別負責的職責

就我的觀點類別生命週期的控制並不應該視為一個職責,況且 1. 與 2. 並沒有直接的依賴性而導致類別運作或測試之間有高耦合性。

反而應該專注於 2. 類別本身所負責的職責是否過多或有過高的耦合性,以至於類別職責依賴性高難以測試又容易出問題。

SRP 原則的可以參考以下文章

3. Singleton Pattern 違反 OCP 原則

If a singleton class allows inheritance, and is “open” for extension, then it can no longer enforce the singleton pattern.

同樣是節錄自Stackoverflow 的問題,就我的觀點 OCP 原則 是指類別可以容易地被擴展,擴展類別時不需要去更改原有的程式碼,並非所指 “繼承時失去 Singleton 的特性(要修改 constructor)”。

對於此問題其實在原問題連結下方,我認為有比較正確的解釋

It just happens to be most commonly implemented in violation of the OCP by developers. by jaco0646

總結

  • 使用 Singleton Pattern 應該減少 Global state 的產生。
  • 使用 Singleton Pattern 應該注意是否帶來了高耦合性。
  • 綜合以上兩點,適時的使用 dependency injection 增加可測試性。

在應用面上,還是有許多場景是相當適合 Singleton Pattern

例如:

  • Logger / Random 等等的 Immutable 物件與行為。
  • 特定應用面的全域設定物件或儲存結構。

最後,設計模式都有適合的場景及用法,不要為了設計模式而設計模式。

Right idea, wrong tool drawing (https://fritsahlefeldt.files.wordpress.com/2019/08/2d839-nail-screw.jpg)

以上是我對於 Singleton Pattern 的介紹與閱讀過程中的心得與想法,大家如果有任何的不同想法再請不吝提出 :D

參考資源

--

--