閒談軟體設計:Singleton

閱讀時間約 9 分鐘
用 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 的開發者文件,覺得好陌生啊~
avatar-img
53會員
104內容數
這是從 Medium 開始的一個專題,主要是想用輕鬆閒談的方式,分享這幾年軟體開發的心得,原本比較侷限於軟體架構,但這幾年的文章不僅限於架構,也聊不少流程相關的心得,所以趁換平台,順勢換成閒談軟體設計。
留言0
查看全部
avatar-img
發表第一個留言支持創作者!
Spirit的沙龍 的其他內容
在上回提到一些應該要避免的措施,以及時時梳理 product backlog 讓團隊有較好的估算,這回則是作為一位 scrum master,我們該如何自省與發現估算的問題,也是以自我反省的方式完結這個系列。
真的要符合 single responsibility,通常會得到很多很小的類別或是函式,各別完成一個小的功能,然後在某個地方被聚合起來完成一個使用案例 (use case),而不是一個很大的類別,包山包海,然後最後變成一個狀態超複雜,超級難測試的類別。
在上回討論 Scrum 對於估算的精神與常見的估算單位,這回就來討論一些應該避免的事項,讓團隊能有更好的估算,下回則是過去的自省與感想。要讓團隊有較高品質的估算,agile coach 或 scrum master 可以觀察一些徵兆,若有發現盡早排除,免得讓團隊成員有壞習慣或是對估算這件事有陰影。
這是幾年來我對於軟體架構師的心路歷程,上述不保證讓你成為軟體架構師,但希望會對軟體工程師職涯有幫助。也希望台灣的軟體公司能稍微多注重一下軟體架構,甚至能像 91App 不只工程師團隊,還有軟體架構團隊。
這同是 2016 年的舊文,根據現在的閱讀習慣重新整理,文章分成三回陸續發布,本回先談談在 Scrum 中,為什麼要估時,然後談談比較常見的單位與用法。下回則是幾個小方法,讓團隊能有更好的估算。最後一回,則是一些過去的自省與感想。
我覺得好的體驗,大多不需要 production code 有對應的修改或調整,主要來自工具的優化,像是 IDE、CI/CD 等。但接下來討論的體驗,須對 production code 進行修改,甚至影響到撰寫,但帶來的體驗我覺得是有待商榷的。
在上回提到一些應該要避免的措施,以及時時梳理 product backlog 讓團隊有較好的估算,這回則是作為一位 scrum master,我們該如何自省與發現估算的問題,也是以自我反省的方式完結這個系列。
真的要符合 single responsibility,通常會得到很多很小的類別或是函式,各別完成一個小的功能,然後在某個地方被聚合起來完成一個使用案例 (use case),而不是一個很大的類別,包山包海,然後最後變成一個狀態超複雜,超級難測試的類別。
在上回討論 Scrum 對於估算的精神與常見的估算單位,這回就來討論一些應該避免的事項,讓團隊能有更好的估算,下回則是過去的自省與感想。要讓團隊有較高品質的估算,agile coach 或 scrum master 可以觀察一些徵兆,若有發現盡早排除,免得讓團隊成員有壞習慣或是對估算這件事有陰影。
這是幾年來我對於軟體架構師的心路歷程,上述不保證讓你成為軟體架構師,但希望會對軟體工程師職涯有幫助。也希望台灣的軟體公司能稍微多注重一下軟體架構,甚至能像 91App 不只工程師團隊,還有軟體架構團隊。
這同是 2016 年的舊文,根據現在的閱讀習慣重新整理,文章分成三回陸續發布,本回先談談在 Scrum 中,為什麼要估時,然後談談比較常見的單位與用法。下回則是幾個小方法,讓團隊能有更好的估算。最後一回,則是一些過去的自省與感想。
我覺得好的體驗,大多不需要 production code 有對應的修改或調整,主要來自工具的優化,像是 IDE、CI/CD 等。但接下來討論的體驗,須對 production code 進行修改,甚至影響到撰寫,但帶來的體驗我覺得是有待商榷的。
你可能也想看
Google News 追蹤
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
11/20日NVDA即將公布最新一期的財報, 今天Sell Side的分析師, 開始調高目標價, 市場的股價也開始反應, 未來一週NVDA將重新回到美股市場的焦點, 今天我們要分析NVDA Sell Side怎麼看待這次NVDA的財報預測, 以及實際上Buy Side的倉位及操作, 從
Thumbnail
Hi 大家好,我是Ethan😊 相近大家都知道保濕是皮膚保養中最基本,也是最重要的一步。無論是在畫室裡長時間對著畫布,還是在旅途中面對各種氣候變化,保持皮膚的水分平衡對我來說至關重要。保濕化妝水不僅能迅速為皮膚補水,還能提升後續保養品的吸收效率。 曾經,我的保養程序簡單到只包括清潔和隨意上乳液
Thumbnail
這篇文章探討了在軟體開發中的技術債可能來自哪些原因,以及如何自動化偵測與修復技術債。作者透過分享不同情境下的技術債選擇,提供了對於技術債的思考與建議,針對開發人員在需要做出無奈的技術決策時,提供了一些建議。此外,還提供了一些在做出技術決策時的方法,如保留抽象層和避免vendor lock-in。
Thumbnail
今天來聊個最近很夯的主題 DDD,但不是 DDD 的本尊 Domain Driven Design,而是無所不在的 Database Driven Design,Database Driven Design 不是不好,只是你的模型容易變成貧血模型,邏輯都集中在 service 層等等。
Thumbnail
有趣的是,Model 其實沒什麼嚴格的定義,所以每個人對 Model 的解讀也不盡相同,有人覺得資料怎麼儲存屬於 Model 的一部份 (受 ORM 工具的影響),有人覺得工作流程 (workflow) 是 Model 的一部份,我個人也有自己的想法,而且隨專案的規模和特性,也不是總是一樣的。
Thumbnail
起源是當時 Facebook 有篇文章討論不少人分不清楚上述二者的差別,當時寫了首部曲《閒談軟體設計:API Naming Style》,接著是《閒談軟體設計:內部函式庫》,但始終沒談到 library 和 framework 的差別,主要是沒有好的例子,這次這例子還蠻不錯的。
Thumbnail
我自己偏好用 Repository 搭配 decorator 來管理 cache,而不是在 controller 層或是到處都有快取的邏輯,如果程式都是透過 Repository 更新資料,Repository 就會是一個不錯的地方更新快取,邏輯也就不會散亂在各處了。
Thumbnail
最近身旁有幾位正在懷孕、或剛生產完的朋友,讓我想起自己在懷孕期間印象最深刻的三件「怪事」,其中又以第三件事最誇張。
Thumbnail
不知道大家在買房之前是不是都會參考親朋好友的意見,或是上網看一些買房注意事項,有時候考慮了這塊就忘了那塊,考慮的那塊又忘了這塊.......
Thumbnail
塔西佗陷阱(Tacitus Trap),一個得名於古羅馬歷史學家塔西佗的政治學理論,意指倘若公權力失去其公信力,無論如何發言或是處事,社會均將給予其負面評價。 當然信著恆信、不信者恆不信,這就是真實的人生。
Thumbnail
關於片名   台灣片名《花漾女子》,原文片名《Promising Young Woman》,台灣譯名將時間定格在悲劇發生前,而原文片名則進一步帶我們看見另一個可能性結果
Thumbnail
前幾年因為身體的關係,當了幾年的律師逃兵,當時開了之前的事務所以後,一時間也沒有特別想要做甚麼事情,所以就邊讀一點書、早晚運動一下,剛好聽到當年同梯朋友進去金融業工作,因此也抱著嘗(ㄊㄠˊ)試(ㄅㄧˋ)的心態,找了份銀行法令遵循的工作
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
11/20日NVDA即將公布最新一期的財報, 今天Sell Side的分析師, 開始調高目標價, 市場的股價也開始反應, 未來一週NVDA將重新回到美股市場的焦點, 今天我們要分析NVDA Sell Side怎麼看待這次NVDA的財報預測, 以及實際上Buy Side的倉位及操作, 從
Thumbnail
Hi 大家好,我是Ethan😊 相近大家都知道保濕是皮膚保養中最基本,也是最重要的一步。無論是在畫室裡長時間對著畫布,還是在旅途中面對各種氣候變化,保持皮膚的水分平衡對我來說至關重要。保濕化妝水不僅能迅速為皮膚補水,還能提升後續保養品的吸收效率。 曾經,我的保養程序簡單到只包括清潔和隨意上乳液
Thumbnail
這篇文章探討了在軟體開發中的技術債可能來自哪些原因,以及如何自動化偵測與修復技術債。作者透過分享不同情境下的技術債選擇,提供了對於技術債的思考與建議,針對開發人員在需要做出無奈的技術決策時,提供了一些建議。此外,還提供了一些在做出技術決策時的方法,如保留抽象層和避免vendor lock-in。
Thumbnail
今天來聊個最近很夯的主題 DDD,但不是 DDD 的本尊 Domain Driven Design,而是無所不在的 Database Driven Design,Database Driven Design 不是不好,只是你的模型容易變成貧血模型,邏輯都集中在 service 層等等。
Thumbnail
有趣的是,Model 其實沒什麼嚴格的定義,所以每個人對 Model 的解讀也不盡相同,有人覺得資料怎麼儲存屬於 Model 的一部份 (受 ORM 工具的影響),有人覺得工作流程 (workflow) 是 Model 的一部份,我個人也有自己的想法,而且隨專案的規模和特性,也不是總是一樣的。
Thumbnail
起源是當時 Facebook 有篇文章討論不少人分不清楚上述二者的差別,當時寫了首部曲《閒談軟體設計:API Naming Style》,接著是《閒談軟體設計:內部函式庫》,但始終沒談到 library 和 framework 的差別,主要是沒有好的例子,這次這例子還蠻不錯的。
Thumbnail
我自己偏好用 Repository 搭配 decorator 來管理 cache,而不是在 controller 層或是到處都有快取的邏輯,如果程式都是透過 Repository 更新資料,Repository 就會是一個不錯的地方更新快取,邏輯也就不會散亂在各處了。
Thumbnail
最近身旁有幾位正在懷孕、或剛生產完的朋友,讓我想起自己在懷孕期間印象最深刻的三件「怪事」,其中又以第三件事最誇張。
Thumbnail
不知道大家在買房之前是不是都會參考親朋好友的意見,或是上網看一些買房注意事項,有時候考慮了這塊就忘了那塊,考慮的那塊又忘了這塊.......
Thumbnail
塔西佗陷阱(Tacitus Trap),一個得名於古羅馬歷史學家塔西佗的政治學理論,意指倘若公權力失去其公信力,無論如何發言或是處事,社會均將給予其負面評價。 當然信著恆信、不信者恆不信,這就是真實的人生。
Thumbnail
關於片名   台灣片名《花漾女子》,原文片名《Promising Young Woman》,台灣譯名將時間定格在悲劇發生前,而原文片名則進一步帶我們看見另一個可能性結果
Thumbnail
前幾年因為身體的關係,當了幾年的律師逃兵,當時開了之前的事務所以後,一時間也沒有特別想要做甚麼事情,所以就邊讀一點書、早晚運動一下,剛好聽到當年同梯朋友進去金融業工作,因此也抱著嘗(ㄊㄠˊ)試(ㄅㄧˋ)的心態,找了份銀行法令遵循的工作