獻給曾懷疑 Singleton 真的適合一直這樣使用的人
做 iOS 開發時,應該沒有人完全沒用過 Singleton。我也一樣。SomeManager.shared 這種程式碼在每個專案裡至少都會有好幾段。
但從某個時候開始,這個.shared逐漸變得令人不舒服。測試時狀態會互相干擾,團隊成員也會從各處任意存取它。
後來我認識了 Monostate Pattern。今天想分享我親自使用 Swift Monostate 的經驗,以及它是否真的能成為 Singleton 的替代方案。
先說結論,Monostate 是「共享同一份狀態,但允許建立多個執行個體」的模式。目的和 Singleton 相同,但做法完全相反。不過它不是萬靈丹,還是要看情境。下面會逐一說明。
Monostate Pattern 到底是什麼?
Singleton 的核心是「只建立一個執行個體」。限制初始化器,讓程式只使用shared。
Monostate 則相反。執行個體可以建立任意多個,但其中的狀態(資料)全部透過 static 共享。
也就是說,不論建立物件 A 或 B,兩者看到的資料都在同一處。外觀看起來是普通物件,內部卻連接到同一份狀態。
用 Swift 簡單表示,大致是這個樣子。
struct Settings {
private static var _volume = 50 // 共享 static狀態
var volume: Int { // 像執行個體一樣使用
get { Settings._volume }
set { Settings._volume = newValue }
}
}
// 不同執行個體,但狀態只有一份
var a = Settings(); a.volume = 80
print(Settings().volume) // 80
即使重新建立Settings(),volume 仍會是 80。執行個體有多個,但狀態只有一份,這就是 Monostate 的全部。
它和 Singleton 有什麼不同?
最大的差異在於使用端的程式碼。
使用 Singleton 時,呼叫端必須一直意識到.shared,心裡要想著「啊,這是 Singleton」。Monostate 則只要像普通物件一樣建立Settings()並使用,使用端不需要知道內部實作。
整理成表格如下。
| 分類 | Singleton | Monostate |
|---|---|---|
| 執行個體數量 | 只有 1 個 | 可建立多個 |
| 共享的內容 | 執行個體本身 | 狀態(static 資料) |
| 使用端 | 明確指定.shared |
像普通物件一樣 |
| 繼承 | 棘手 | 相對自由 |
尤其是繼承很有意思。Singleton 很難繼承,但 Monostate 是一般型別,因此擴充起來更加靈活。
那麼,它能成為 Singleton 的替代方案嗎?
老實說,有時候可以,有時候不行。
先談優點。使用端的程式碼會更簡潔,.shared這個全域存取點也不會散落在程式碼各處。介面和普通物件完全一樣,因此日後變更結構時負擔也比較小。
Monostate 是「隱藏全域狀態」的模式。看似方便,但隱藏的全域狀態本身就是一把雙面刃。
問題就在這裡。它外觀看起來像普通物件,團隊成員可能以為「這只是區域物件」而建立它,結果才發現狀態其實是全域共享的,反而更容易混淆。
而且因為是 static 共享,在多執行緒環境中仍然要同樣注意並行處理問題。這一點和 Singleton 沒有差別。
依我的經驗,在現代 Swift 中,我建議優先考慮相依性注入,而不是 Monostate。這樣更容易測試,也能讓相依關係更清楚。
那麼,什麼時候適合使用呢?
總結來說,適合以下情況。
- 現有 Singleton 程式碼太多,想逐步移除時
- 想讓使用端介面維持像普通物件一樣簡潔時
- 需要處理必須支援繼承或擴充的全域狀態時
反過來說,如果是從零開始撰寫新專案,我會建議先考慮相依性注入或明確的狀態管理,而不是 Monostate。
歸根究柢,Monostate 不是「Singleton 的完美上位替代」,而是「個性不同的另一個選項」。把它放進工具箱,在適合的情況下拿出來使用即可。
希望今天的內容能為疲於應付.shared濫用的人提供一點小提示。與其盲目改用其他方案,不如花點時間想想什麼最適合自己的專案。

