2023-11-03|閱讀時間 ‧ 約 10 分鐘

閒談軟體設計:Singleton

用 Singleton 找圖,都只會找到單一純麥威士忌 XD

用 Singleton 找圖,都只會找到單一純麥威士忌 XD

這文章來自網友在閒談軟體設計:Single Responsibility 在 Medium 上的留言 (有人幫忙想題目也挺不錯的),問到:Singleton 對於好的架構來說是否能避免就避免呢?我簡單地回了一下我的想法 ,但 Singleton 其實很有趣,所以就寫篇文章來聊聊吧!

意圖很重要

我個人很少用 Singleton,倒不是因為它是全域變數 (這等會再談),或是有人說它是個 anti pattern,主要是我對於使用任何 pattern 都是很謹慎的,確認所在的 context (或是 forces) 以及希望達成的意圖 (intent) 後,才決定是否使用 pattern 及使用哪個 pattern,單純以 Singleton 來說,至少要滿足

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

我才會考慮使用 Singleton。而且重點應該是 and 前的那一句,後面那一句對我來說不太重要。

但真的是只有一個實體的時候才能用 Singleton 嗎?這讓我想起當年博士資格考有一題,大意是:請替作業系統設計一個程式介面,操作最多只有五個的硬體資源。當時我的設計先用一個類別代表硬體資源,然後用 Singleton,透過 global 的 access method 存取有限的物件實體來操作硬體資源。事實上,在《Design Patterns》書中有這樣一段描述 (p. 128):

Permits a variable number of instances: The pattern makes it easy to change your mind and allow more than one instance of the Singleton class. Moreover, you can use the same approach to control the number of instances that the application uses. Only the operation that grants access to the Singleton instance needs to change.

也就是說,Singleton 主要是用來限制一個類別能生成的實體數量,只要不是這個目的,是不需要使用 Singleton 的。但有趣的是,我畢業後進到業界,卻到處看到 Singleton,而且 Android App 的情況特別明顯,但若問原作者,得到的答案都是為了 provide a global point to access to it

如何避免

想想原因很簡單,Android 的框架設計控制 Activity 和 Fragment 的物件生成 (參閱閒談軟體設計:Plug-in),這讓 dependency injection 的幾個常見方式,瞬間失去了 Constructor Injection 和 Setter Injection 兩種方式,剩下的多少需要第三方框架的協助,若不想使用第三方框架,就只剩下 Singleton 是較簡單的方式。

不使用第三方框架的情況下,我曾嘗試過 Interface Injection 搭配 Android 的 ActivityLifecycleCallbacks (Android 4.0 以上才能使用),在 activity 建立後,替有實作 AppCoreAware) 的 activity 注入相依:

事實上,AppCore 在整個 app 的生命週期中 (注意,不是 activity 的生命週期) 也只有一個實體而已,但我卻不是使用 Singleton。說真的,這樣的方式並不是沒有缺點,還是有些限制,像是不能在 activity 的 onCreate 使用 core 物件,失去一個進行初始化的時機點,得延遲到 onStart 的時候。

那為什麼要這樣做呢?測試不好替換 mock,如果程式直接使用 instance 取得 AppCore 實體,那要如何在測試的時候換掉 instance 的回傳值呢?

這就牽扯到剛剛提到的,Singleton 到底是不是一個全域變數?在 Java 中因為所有的東西都要放在 class/interface 中,所以沒有真正的全域變數,但一般來說,會把 line 3 的 startedActivities 視為全域變數,因為程式中到處都可以讀取及修改它的內容,但會把 line 4 的maxStartableActivities 視為常數,因為無法修改它的內容

小心使用

那 Singleton 呢?line 7 宣告 instance 時沒有加上 final 修飾字,所以它確實是一個變數,但因為宣告成 private 變數,且又沒有任何 setter 可以修改它,在第一個實體初始化後,它就是個受保護的實體,要稱它為全域變數好像又有點怪怪的。只有在什麼狀況它會變成全域變數呢?提供 setter 可以換掉它,通常就是因為測試的時候想替換 mock 才會提供 setter,既然知道全域變數不好,那提供 setter 就不是一個好的設計,注意,是提供 setter 不好,而不是 Singleton 不好

那如果 Singleton 的實體本身有 setter 可以修改實體內部的狀態呢?內部狀態會是全域變數嗎?是!但老實說,我覺得問題不是 Singleton,而是 setter,想當年學 OOP,用 setter 就像是犯了什麼大忌一樣,都是深思熟慮後才會使用,但現在不知道為什麼?想也不想地就替類別新增 setter。針對這問題,我建議讀一下 Kent Beck 的《Implementation Patterns》第八章的 Method Visibility 和 Setting Method。《Refactoring to Patterns》的 6.6 節中也提到 Kent Beck 對 Singleton 的看法:

Singletons 的真正問題在於它們給你一個很好的藉口,讓你不去仔細思考物件的適當可視性 (appropriate visibility),「找出物件曝光與保護 (exposure and protection) 之間的正確平衡點」對維持彈性而言至關重要。

因此,若要使用 Singleton,我是不會提供 setter,反而是在使用 Singleton 的 client 端透過一些方式,解開與 Singleton 的直接耦合,來增加 client 的可測試性,例如:使用 constructor overloading,提供額外注入相依的方式。

其他注意事項

Singleton 說起來好像很簡單,但實作上卻有很多眉角要特別注意,例如:要在多執行緒存取的情況下,依然能維持只有一個實體,在 Java 中還可以透過 synchronized 關鍵字幫上不少忙,但在 C++ 中就很麻煩,因為 lock 本身就要是一個 Singleton

現代很多新的語言不用管理記憶體,省去使用 Singleton 時可能遇到的一堆麻煩,在《Pattern Hatching: Design Patterns Applied》的 3.1 節中,就探討不少 C++ 在 Singleton 記憶體管理上的難處,有興趣可以讀讀。

最後看一下隱形的 Singleton,有用過 Spring framework 的開發者應該都寫過像這樣的 Spring bean 初始化函式:

若設置中斷點在 line 3 及 line 9,然後用 Debugger 啟動程式,當程式停在第一個中斷點時記下 accounts 參數的記憶體位置,然後讓程式繼續執行,等到第二個中斷點停下時,再記錄一次 accounts 參數的記憶體位置,你會發現這兩個的記憶體位置是一樣的。

基本上 Spring framework 在初始化任何有 @Bean@Repository 等標註的物件,都會以 Singleton 的方式處理。然後用類似剛剛提到的 Interface Injection 注入相依,差別只在 Spring framework 不是檢查有沒有實作某個 interface,而是檢查有沒有使用 @Autowired

但要使用 @Autowired,類別本身也須加上 @Bean 等 annotation。為了不讓這些 annotation 汙染 domain model,我都是用上述的方式將 domain model 變成一個 Spring bean,而不是在 domain model 中加入 Spring framework 的 annotation。

總結

我不會把 Singleton 視為一個 anti pattern,它確實有存在的需要,只是平常開發通常不會有使用到它的 intent,因此我個人很少使用它,若要使用也盡可能避免直接相依。若因為誤用而身受其害,可以參考《Refactoring to Patterns》的 6.6 節 Inline Singleton 將 Singleton 移除 [註:Inline Singleton 是書中少數三個遠離 Design Pattern 的方法之一]。


話說,因為已經好幾年沒開發 Android 程式,為了寫範例,去翻了一下 Android 的開發者文件,覺得好陌生啊~
分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.