隨著 iOS App 規模變大,你可能也曾因為相依性管理而傷透腦筋。
最後一定會遇到的,就是Swift Service Locator 模式。有人覺得方便,也有人認為它是反模式而避之唯恐不及。
比起全面導入,我發現只在真正需要的地方局部使用,效果更好。
先說結論:
Service Locator 並不是 DI(Dependency Injection,相依性注入)的完整替代方案;使用不當時,會變成隱藏相依性的反模式。它比較像是一種輔助工具。
今天就以我的經驗為主,說明這個模式是什麼、為什麼常被批評,以及什麼時候仍然值得使用。
什麼是 Service Locator 模式?
一句話來說,就是從中央註冊庫取出需要的物件來使用的方法。
在某處建立一個「註冊庫」,每當需要物件時,就從裡面尋找並使用。
如果 Constructor Injection 是從外部注入相依性的方式,那麼 Service Locator 就是在內部直接取出相依性來使用。
看過程式碼後應該很快就能掌握概念。
// 將服務註冊到中央註冊庫,再取出使用的結構
final class ServiceLocator {
static let shared = ServiceLocator()
private var services: [String: Any] = [:]
func register<T>(_ service: T) { services["\(T.self)"] = service }
func resolve<T>() -> T { services["\(T.self)"] as! T }
}
在 App 啟動時註冊一次即可。
實際使用的一方則會像這樣取出所需項目。
// 在 ViewModel 內部直接「尋找」相依性
final class FeedViewModel {
private let api: APIClient = ServiceLocator.shared.resolve()
// 即使不將 api放入初始化器,也能運作
}
如你所見,最大的優點就是初始化器變得更簡潔。
為什麼 Service Locator 被稱為反模式?
最大的原因只有一個:相依性會被隱藏。
再看一次上面的FeedViewModel程式碼。只看初始化器,根本不知道這個類別使用了APIClient。
必須打開內部實作才看得出來。這在協作時會造成相當大的不便。
第二個問題是測試。
使用 Constructor Injection 時,測試只要直接傳入 Mock 即可。但 Service Locator 必須修改全域註冊庫的狀態,因此測試之間很容易互相影響。
第三個是執行階段風險。
嘗試取出忘記註冊的相依性時,不會在編譯時發現,而是 App 執行期間發生當機。上方範例的強制轉型as!正是問題所在。
總結如下。
| 比較項目 | Constructor Injection(DI) | Service Locator |
|---|---|---|
| 相依性揭露 | 清楚呈現 | 隱藏在內部 |
| 測試便利性 | 高 | 偏低 |
| 錯誤發現時機 | 編譯時 | 執行階段 |
| 初始化器簡潔度 | 參數變多 | 整潔 |
那麼,什麼時候使用也沒關係呢?
它並非絕對不好。我曾在以下情境中覺得很實用。
- 在整個 App 共用的單一服務(記錄器、分析工具等)
- Constructor Injection 路徑太深,參數不斷被往下傳遞時
- 逐步將 DI 導入舊版程式碼的過渡期
尤其第三種情況很符合實務。
要一次把 Constructor Injection 全部加入既有程式碼,負擔很大。這時先用 Service Locator 搭一座臨時的橋,再逐步移轉到 Constructor Injection,會比較安全。
最近比起純粹的 Service Locator,更常使用Swinject或 Swift 的@Environment等 DI 容器。
它們內部同樣是從註冊庫取出物件,但註冊驗證與範圍管理更完善,因此能降低風險。
常見問題
Q. 和 Singleton 有什麼不同?
Singleton 是讓某一個特定物件成為全域物件;Service Locator 則像是存放多個物件的「倉庫」。兩者性質不太相同。
Q. SwiftUI 也會使用嗎?
SwiftUI 的@Environment與@EnvironmentObject其實和 Service Locator 是非常相似的概念。也可以說 Apple 在框架層級提供了類似的方式。
Q. 所以預設應該採用什麼?
建議預設使用 Constructor Injection。Service Locator 只在確實需要的地方局部使用。
相依性管理沒有唯一正解,但方向很明確。
盡可能讓相依性清楚呈現,隱藏相依性的工具則只在必要處謹慎使用。只要做到這點,維護就會輕鬆許多。

