單例與單例項之爭

BEASTQ發表於2016-02-23

今天,在 DevMonologue 我們將要討論一個設計概念,它已經困擾我有一段時間了。它不僅僅關係到 iOS 開發,在程式設計中也具有普遍意義。

我不會說我是軟體架構的專家。但根據我的經驗,我發現單例(singleton)與單例項( single instance)之爭問題是許多專案都會遇到的,而很多人並沒有意識到這一點。

這就是為什麼我想分享一些想法,關於如何避免重大的設計缺陷的原因。正如我所說,我並不一定能保證這裡寫的內容在所有情況下都是 100% 正確的,所以歡迎大家慷慨反饋。我很欣慰看到這篇文章變成大家分享經驗的討論稿,或許還能在這領域給一些常見問題提供解決方法。所以,不要羞澀,在討論區給我們寫幾行評論吧。

什麼是“單例與單例項之爭”的一派胡言?

廢話有點多了,讓我們言歸正傳。我所說的“單例與單例項之爭”是什麼意思呢?

“單例與單例項之爭”的起源

現在,我不確定(在歷史上)誰引進了這個詞,但我可以告訴你,我第一次看到它是在哪。這是在一本叫《Working effectively with legacy code》,由 Michael Feathers 所著的書。如果你還不知道這本書,我建議你去查查。書中有大量實用的技巧,即使你覺得你的工作不接觸遺留程式碼,你也會從那本書中獲益良多,相信我。

定義

“單例與單例項之爭”背後的原理非常簡單。它只是指明瞭無論什麼情況下,在平常使用單例物件時,你應該考慮只用一個單例項的情況。這是什麼意思呢?

一個單例是一個嚴格遵守規則的類,該規則規定,該類只能有一個物件,並且更多的時候,該物件在整個程式都可以被訪問。

另一方面,一個“單例項”,意味著該類本身不是單例,但是(在更高層次上),你要確保該類有唯一一個例項。

初看,這似乎並不重要,甚至單例更適合,因為它的用法更明確。讓我們看看我是否能以其他方式說服你……

負面作用

破壞封裝

在這兩種情況中的類應只有一個物件。但一個單例積極嚴格遵守規則,而“單例項”卻太單純,它只是假設它的使用者會知道不去建立多個物件。大家都知道,當在這樣的場景下,儘可能直接明瞭會更好。所以就封裝而言,單例獲勝。

使用方便

開發者是懶惰的,而且(理所當然)喜歡簡單的介面。並且就儘可能方便而言,你不能打敗一個單例。你所需要做的只是匯入單例類(如果必須的話),然後呼叫返回共享例項的方法。似乎沒有比這更好的了。而“單例項”的使用,你將需要找出誰擁有該物件,以及你如何獲得它。

然而,易於使用不總是件好事。我希望當你閱讀最後一段時提高警覺。更多的內容在後面。

正面作用

在測試驅動開發中的“單例與單例項之爭”

像許多你會在《Working effectively with legacy code》中找到的主題一樣,這一點與測試驅動開發有關。但即使你不認同測試驅動開發,先不要關閉標籤。雖然“單例與單例項之爭”極大地簡化了測試驅動開發,它也是所有專案中的強有力的設計決策。

在一般測試驅動開發中,非常重要的是每個測試需要隔離執行,執行環境需要在各個測試之間復位。這讓單例成為問題。因為在應用的整個生命週期只有一個物件持續存在,你不能確保以前的測試資料不再遺留在環境中(還要注意,大多數的 IDE 可以併發執行單元測試,並且不能確保測試是順序執行的)。有辦法對付這個問題,但通常,當使用測試驅動開發,“單例與單例項之爭”是 0:1。

限制訪問

這就是上文討論的“易於使用”的弊端。但,為什麼我們需要儘量讓它難以訪問一個物件?

那麼,通用訪問有一個固有問題。如果一個應用的所有部分都可以訪問一個物件,那麼當有壞情況發生時它往往很難知道問題是什麼。如果你追蹤一個單例物件方法,而這方法有 30 個其他物件訪問,這並不容易找到問題的來源。

另外一個問題是,你真的讓它對使用你的類的人都易於訪問的話,那麼他們就會不斷地使用它……這導致的正是我前面寫的——每個人似乎都從單例類中呼叫方法。

現在,對於一般通用的功能,這些東西都是可以有的,應該不會造成問題。你不可能從不使用單例。但我覺得人們(包括我)都過度使用它們。我給你舉個例子:

“單例與單例項”問題的一個例子

對我來說,單例設計模式是非常有用的,但不少開發者似乎有些濫用了。當你為你的類選擇使用單例時,你最好在你決定之前認真想清楚它是否真的需要。它是不是絕對只擁有唯一的例項,如果有兩個存在,它是否會破壞你的架構?或者它只是需要保證一個例項就足夠了?

當人們只是需要一個全域性變數,這是單例模式最常見的濫用情況。因為全域性變數在現代程式設計標準中是備受譴責的,但它們仍是有用的,開發者們可能禁不住建立一個單例,這樣該物件就可以在全域性被訪問。所以,允許我從前面的段落問一個問題:

絕對必要只擁有一個例項嗎……或者只是要求一個例項就足夠了?

不,這沒有必要!

然而,這是“單例與單例項之爭”選擇的一個比較粗糙的例子。雖然這樣使用可能不會讓你陷入嚴重的麻煩,但你一定要遠離這種情況。我們需要深入挖掘進入真正的問題。

想想我們可愛的 MVC 模式。尤其是控制器部分——我們的業務邏輯。你見過或參與過多少專案,它們在業務邏輯中使用大量單例的?坦誠面對它。CommunicationsManager、DataManager、NotificationsManager、LoginManager……它們都有可能是單例物件。但它們就應該是嗎?

讓我們再想想最後一個,LoginManager。它可能是一個管理使用者會話的物件——也許包含了一個令牌(token)、cookie、使用者憑據?

大多數應用程式只允許同時單個使用者登入。所以,LoginManager 類真的需要有唯一的物件,對不對?並且單例模式很適合,不是嗎?在這種情況下,允許我問一個問題:

絕對必要只擁有一個例項嗎……或者只是要求一個例項就足夠了?

是的!存在兩個 LoginManagers 會出錯!並且,在這裡有些可疑的事情。這種使用情況會怎樣:

  • 登入
  • 登出
  • 以其他身份重新登入

哦!LoginManager 似乎真的需要擁有唯一的例項,但該例項在應用整個生命週期中可能不是同一個。所以,我們應問的問題實際上是:

絕對需要擁有唯一,且在應用整個生命週期都不改變的例項嗎?

那麼,出現在大多數專案(在我的經歷中)的是,LoginManager 例項(例如)只是在兩次登入之間保持相同。為了有效執行,一旦使用者登出,所有使用者資訊會被從例項中刪除。這應是相當容易的。這能有多難?一個使用者會話只需要通過幾個條件確認——可能一個令牌或者一個使用者名稱。但對於那些預快取的使用者朋友列表或頭像或密碼會怎樣。這種程式碼似乎總在維護期間被破壞。你不能依賴於你的同事(甚至是你)總記住在登出時清除資料。這樣做不合理。

而下一次,當你忘記清除該使用者的令牌會發生什麼?你最終甚至可能登入錯誤的使用者!

現在,如果只有我們的 LoginManager 不是單例……我們只想當使用者登出完成時刪除物件。我們不會那麼擔心類在登出期間可能沒能清除所有資料。

這故事很接近現實,在軟體開發中沒有什麼是永遠的。所以,不要太執著於你的例項物件。否則,它們會把你的小心臟打成碎片!

最後,我覺得這就是今天關於“單例與單例項之爭”獨白吧。感謝你的閱讀!

打賞支援我翻譯更多好文章,謝謝!

打賞譯者

打賞支援我翻譯更多好文章,謝謝!

任選一種支付方式

單例與單例項之爭 單例與單例項之爭

相關文章