閒談軟體設計: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 的開發者文件,覺得好陌生啊~
52會員
102Content count
這是從 Medium 開始的一個專題,主要是想用輕鬆閒談的方式,分享這幾年軟體開發的心得,原本比較侷限於軟體架構,但這幾年的文章不僅限於架構,也聊不少流程相關的心得,所以趁換平台,順勢換成閒談軟體設計。
留言0
查看全部
發表第一個留言支持創作者!
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 進行修改,甚至影響到撰寫,但帶來的體驗我覺得是有待商榷的。
你可能也想看
Thumbnail
重點摘要: 1.9 月降息 2 碼、進一步暗示年內還有 50 bp 降息 2.SEP 上修失業率預期,但快速的降息速率將有助失業率觸頂 3.未來幾個月經濟數據將繼續轉弱,經濟復甦的時點或是 1Q25 季底附近
Thumbnail
近期的「貼文發佈流程 & 版型大更新」功能大家使用了嗎? 新版式整體視覺上「更加凸顯圖片」,為了搭配這次的更新,我們推出首次貼文策展 ❤️ 使用貼文功能並完成這次的指定任務,還有機會獲得富士即可拍,讓你的美好回憶都可以用即可拍珍藏!
Thumbnail
最近身旁有幾位正在懷孕、或剛生產完的朋友,讓我想起自己在懷孕期間印象最深刻的三件「怪事」,其中又以第三件事最誇張。
Thumbnail
不知道大家在買房之前是不是都會參考親朋好友的意見,或是上網看一些買房注意事項,有時候考慮了這塊就忘了那塊,考慮的那塊又忘了這塊.......
Thumbnail
塔西佗陷阱(Tacitus Trap),一個得名於古羅馬歷史學家塔西佗的政治學理論,意指倘若公權力失去其公信力,無論如何發言或是處事,社會均將給予其負面評價。 當然信著恆信、不信者恆不信,這就是真實的人生。
Thumbnail
關於片名   台灣片名《花漾女子》,原文片名《Promising Young Woman》,台灣譯名將時間定格在悲劇發生前,而原文片名則進一步帶我們看見另一個可能性結果
Thumbnail
前幾年因為身體的關係,當了幾年的律師逃兵,當時開了之前的事務所以後,一時間也沒有特別想要做甚麼事情,所以就邊讀一點書、早晚運動一下,剛好聽到當年同梯朋友進去金融業工作,因此也抱著嘗(ㄊㄠˊ)試(ㄅㄧˋ)的心態,找了份銀行法令遵循的工作
Thumbnail
那天我問隊友,怎樣才算是一部小說呢?按字數計算嗎? 他說:「故事內起承轉合都有,就算」 所以我大膽地按著他的標準,將自己寫過的故事,粗略整理出一個明細。
纏中說禪,本名李彪,專欄筆名木子,其人是中國股市比較早期的操盤手,所以他比較熟悉a股的市場情況。他以“纏中說禪”為筆名,從2002年開始寫博客,直到2008年癌症病重停更,期間寫下了不少文章。而博客文章中最為著名的就是他的“教你炒股票”系列文章,他在這個系列裡講到的炒股理論和方法被粉絲稱為“纏論
Thumbnail
婚姻是人生大事,對溥儀尤其如此,因為如果皇帝大婚,就代表溥儀可以脫離眾多便宜老媽的束縛而得以親政。 但詭異的是,這個可以讓他脫離便宜老媽掌控的婚姻,卻還是要由便宜老媽進行主導並且居中角力......
Thumbnail
上次我提到:溥儀就是個死小孩。其實這不能全怪溥儀,而要怪詭異的宮廷教育及生活制度......
Thumbnail
近期電影「末代皇帝」重新修復上映。 為了推坑這部經典之作,本人決定以溥儀本身的自傳《我的前半生》為主要基底,和大家談一些電影中礙於篇幅或是藝術改編,而不容易察覺或是沒有呈現的真實歷史。
Thumbnail
重點摘要: 1.9 月降息 2 碼、進一步暗示年內還有 50 bp 降息 2.SEP 上修失業率預期,但快速的降息速率將有助失業率觸頂 3.未來幾個月經濟數據將繼續轉弱,經濟復甦的時點或是 1Q25 季底附近
Thumbnail
近期的「貼文發佈流程 & 版型大更新」功能大家使用了嗎? 新版式整體視覺上「更加凸顯圖片」,為了搭配這次的更新,我們推出首次貼文策展 ❤️ 使用貼文功能並完成這次的指定任務,還有機會獲得富士即可拍,讓你的美好回憶都可以用即可拍珍藏!
Thumbnail
最近身旁有幾位正在懷孕、或剛生產完的朋友,讓我想起自己在懷孕期間印象最深刻的三件「怪事」,其中又以第三件事最誇張。
Thumbnail
不知道大家在買房之前是不是都會參考親朋好友的意見,或是上網看一些買房注意事項,有時候考慮了這塊就忘了那塊,考慮的那塊又忘了這塊.......
Thumbnail
塔西佗陷阱(Tacitus Trap),一個得名於古羅馬歷史學家塔西佗的政治學理論,意指倘若公權力失去其公信力,無論如何發言或是處事,社會均將給予其負面評價。 當然信著恆信、不信者恆不信,這就是真實的人生。
Thumbnail
關於片名   台灣片名《花漾女子》,原文片名《Promising Young Woman》,台灣譯名將時間定格在悲劇發生前,而原文片名則進一步帶我們看見另一個可能性結果
Thumbnail
前幾年因為身體的關係,當了幾年的律師逃兵,當時開了之前的事務所以後,一時間也沒有特別想要做甚麼事情,所以就邊讀一點書、早晚運動一下,剛好聽到當年同梯朋友進去金融業工作,因此也抱著嘗(ㄊㄠˊ)試(ㄅㄧˋ)的心態,找了份銀行法令遵循的工作
Thumbnail
那天我問隊友,怎樣才算是一部小說呢?按字數計算嗎? 他說:「故事內起承轉合都有,就算」 所以我大膽地按著他的標準,將自己寫過的故事,粗略整理出一個明細。
纏中說禪,本名李彪,專欄筆名木子,其人是中國股市比較早期的操盤手,所以他比較熟悉a股的市場情況。他以“纏中說禪”為筆名,從2002年開始寫博客,直到2008年癌症病重停更,期間寫下了不少文章。而博客文章中最為著名的就是他的“教你炒股票”系列文章,他在這個系列裡講到的炒股理論和方法被粉絲稱為“纏論
Thumbnail
婚姻是人生大事,對溥儀尤其如此,因為如果皇帝大婚,就代表溥儀可以脫離眾多便宜老媽的束縛而得以親政。 但詭異的是,這個可以讓他脫離便宜老媽掌控的婚姻,卻還是要由便宜老媽進行主導並且居中角力......
Thumbnail
上次我提到:溥儀就是個死小孩。其實這不能全怪溥儀,而要怪詭異的宮廷教育及生活制度......
Thumbnail
近期電影「末代皇帝」重新修復上映。 為了推坑這部經典之作,本人決定以溥儀本身的自傳《我的前半生》為主要基底,和大家談一些電影中礙於篇幅或是藝術改編,而不容易察覺或是沒有呈現的真實歷史。