閒談軟體設計: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
大家好,我是woody,是一名料理創作者,非常努力地在嘗試將複雜的料理簡單化,讓大家也可以體驗到料理的樂趣而我也非常享受料理的過程,今天想跟大家聊聊,除了料理本身,料理創作背後的成本。
Thumbnail
哈囉~很久沒跟各位自我介紹一下了~ 大家好~我是爺恩 我是一名圖文插畫家,有追蹤我一段時間的應該有發現爺恩這個品牌經營了好像.....快五年了(汗)時間過得真快!隨著時間過去,創作這件事好像變得更忙碌了,也很開心跟很多厲害的創作者以及廠商互相合作幫忙,還有最重要的是大家的支持與陪伴🥹。  
Thumbnail
嘿,大家新年快樂~ 新年大家都在做什麼呢? 跨年夜的我趕工製作某個外包設計案,在工作告一段落時趕上倒數。 然後和兩個小孩過了一個忙亂的元旦。在深夜時刻,看到朋友傳來的解籤網站,興致勃勃熬夜體驗了一下,覺得非常好玩,或許有人玩過了,但還是想寫上來分享紀錄一下~
在寫程式的過程中,你是否遇過一個類別或模組負責了太多事情,結果導致程式變得難以維護?這類情況經常被稱為「巨石類別 (God Class)」。當我們對這樣的類別做出任何變更時,改動可能會牽一髮動全身影響其他部分,這時候「單一職責原則 (SRP)」便派上用場。 單一職責原則是什麼? 簡單來說,單一
Thumbnail
獨立遊戲開發,很多時是一個想法和熱情冒出來後,就叫人衝到電腦前想快點把成品弄完……就算做不了完整版,至少也想有個Demo。 既然想「快點」,那還要不要「花額外時間」去寫企劃書呢? 在處理這個問題之前,要先理解的是,這句話裡面至少有兩種情況。   其一:團隊裡只有一個企劃,所有事情都由一人主導
Thumbnail
這篇文章介紹了面試時以及開始工作後可能會遇到的問題,包括物件導向OOP、SOLID 設計原則、測試方式,以及 Cookie、Session 與 Cache 的相似處與不同處。提供了豐富的相關資訊。
※ 單例模式介紹 ※ 定義:單例模式是一種設計模式,確保一個class(類)只有一個實例,並提供一個存取它的全域存取點。無論如何取值,皆只對這個實例取值。 ※ 目的:保證一個類別只會產生一個物件,而且提供存取該物件的統一方法。 ※ 講解:單例模式確保一個類無論怎麼 new 或 get,都只能拿
※ 設計模式的五大精神介紹(S.O.L.I.D): ※ 第一大精神 — S:單一職責原則(Single responsibility principle, SRP) ※ 定義: 每個物件,不管是類別或函數,都應該只負責一項功能。 當需求改變時,僅需改相關的區域,而不需要更動其他不相關的部分
※ OPP第三大核心-多型 ※ 多型的基本定義: 多型是利用繼承的特性,讓不同的子類別可以實現相同的介面,但在呼叫這些介面的方法時會表現出不同的行為。這使得程式設計更具彈性和擴展性,避免了複雜的條件判斷式,同時促進了代碼的重用。 class Animal { makeSound() {
當我們太常過於傾向於使用標準制
Thumbnail
達利歐認為,如果沒有建立自己的「原則」,那麼在每天在因應諸多大小狀況時,就必須每一次都像第一次碰到一樣,被迫個別、快速且不假思索地因應。但「太陽下無新事」這句話適用在大多數情況,只有極少數的情境是獨一無二、無法歸類的。如果我們發展出妥善處理大多數重複情境的優質原則,就能更加迅速地做出高品質的決策。
怎麼去定義我所謂的單純呢?比方說,我有兩位同學,是在同一區長大的,但是他們都選擇了自我了斷的方式,結束了生命。所以那區的房子,我是怎麼都不會去買的,無論建材多好、升值空間有多高,並不是說交情有多好,只是機率太高了。冥冥中一定有些事情,在那個地區運𨍭著!有錢買房子,那裡不是買?為什麼要買了以後,再去
Thumbnail
這篇文章介紹了 iOS 中常用的 Design Patterns,包括 MVC、MVVM、Singleton、Delegation、Observer 等。同時比較了 Delegate 和 Notification 的使用時機。參考資料中還有更多相關資訊。
Thumbnail
大家好,我是woody,是一名料理創作者,非常努力地在嘗試將複雜的料理簡單化,讓大家也可以體驗到料理的樂趣而我也非常享受料理的過程,今天想跟大家聊聊,除了料理本身,料理創作背後的成本。
Thumbnail
哈囉~很久沒跟各位自我介紹一下了~ 大家好~我是爺恩 我是一名圖文插畫家,有追蹤我一段時間的應該有發現爺恩這個品牌經營了好像.....快五年了(汗)時間過得真快!隨著時間過去,創作這件事好像變得更忙碌了,也很開心跟很多厲害的創作者以及廠商互相合作幫忙,還有最重要的是大家的支持與陪伴🥹。  
Thumbnail
嘿,大家新年快樂~ 新年大家都在做什麼呢? 跨年夜的我趕工製作某個外包設計案,在工作告一段落時趕上倒數。 然後和兩個小孩過了一個忙亂的元旦。在深夜時刻,看到朋友傳來的解籤網站,興致勃勃熬夜體驗了一下,覺得非常好玩,或許有人玩過了,但還是想寫上來分享紀錄一下~
在寫程式的過程中,你是否遇過一個類別或模組負責了太多事情,結果導致程式變得難以維護?這類情況經常被稱為「巨石類別 (God Class)」。當我們對這樣的類別做出任何變更時,改動可能會牽一髮動全身影響其他部分,這時候「單一職責原則 (SRP)」便派上用場。 單一職責原則是什麼? 簡單來說,單一
Thumbnail
獨立遊戲開發,很多時是一個想法和熱情冒出來後,就叫人衝到電腦前想快點把成品弄完……就算做不了完整版,至少也想有個Demo。 既然想「快點」,那還要不要「花額外時間」去寫企劃書呢? 在處理這個問題之前,要先理解的是,這句話裡面至少有兩種情況。   其一:團隊裡只有一個企劃,所有事情都由一人主導
Thumbnail
這篇文章介紹了面試時以及開始工作後可能會遇到的問題,包括物件導向OOP、SOLID 設計原則、測試方式,以及 Cookie、Session 與 Cache 的相似處與不同處。提供了豐富的相關資訊。
※ 單例模式介紹 ※ 定義:單例模式是一種設計模式,確保一個class(類)只有一個實例,並提供一個存取它的全域存取點。無論如何取值,皆只對這個實例取值。 ※ 目的:保證一個類別只會產生一個物件,而且提供存取該物件的統一方法。 ※ 講解:單例模式確保一個類無論怎麼 new 或 get,都只能拿
※ 設計模式的五大精神介紹(S.O.L.I.D): ※ 第一大精神 — S:單一職責原則(Single responsibility principle, SRP) ※ 定義: 每個物件,不管是類別或函數,都應該只負責一項功能。 當需求改變時,僅需改相關的區域,而不需要更動其他不相關的部分
※ OPP第三大核心-多型 ※ 多型的基本定義: 多型是利用繼承的特性,讓不同的子類別可以實現相同的介面,但在呼叫這些介面的方法時會表現出不同的行為。這使得程式設計更具彈性和擴展性,避免了複雜的條件判斷式,同時促進了代碼的重用。 class Animal { makeSound() {
當我們太常過於傾向於使用標準制
Thumbnail
達利歐認為,如果沒有建立自己的「原則」,那麼在每天在因應諸多大小狀況時,就必須每一次都像第一次碰到一樣,被迫個別、快速且不假思索地因應。但「太陽下無新事」這句話適用在大多數情況,只有極少數的情境是獨一無二、無法歸類的。如果我們發展出妥善處理大多數重複情境的優質原則,就能更加迅速地做出高品質的決策。
怎麼去定義我所謂的單純呢?比方說,我有兩位同學,是在同一區長大的,但是他們都選擇了自我了斷的方式,結束了生命。所以那區的房子,我是怎麼都不會去買的,無論建材多好、升值空間有多高,並不是說交情有多好,只是機率太高了。冥冥中一定有些事情,在那個地區運𨍭著!有錢買房子,那裡不是買?為什麼要買了以後,再去
Thumbnail
這篇文章介紹了 iOS 中常用的 Design Patterns,包括 MVC、MVVM、Singleton、Delegation、Observer 等。同時比較了 Delegate 和 Notification 的使用時機。參考資料中還有更多相關資訊。