軟體設計

Swift Service Locator 模式:DI 的替代方案,還是反模式?(實務總結)

隨著 iOS App 規模變大,你可能也曾因為相依性管理而傷透腦筋。

閱讀 4 分鐘
Swift Service Locator 模式:DI 的替代方案,還是反模式?(實務總結) 封面圖

隨著 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
相依性揭露 清楚呈現 隱藏在內部
測試便利性 偏低
錯誤發現時機 編譯時 執行階段
初始化器簡潔度 參數變多 整潔
只看初始化器看不出使用了什麼,這點一直讓我很在意
只看初始化器看不出使用了什麼,這點一直讓我很在意

那麼,什麼時候使用也沒關係呢?

它並非絕對不好。我曾在以下情境中覺得很實用。

  1. 在整個 App 共用的單一服務(記錄器、分析工具等)
  2. Constructor Injection 路徑太深,參數不斷被往下傳遞時
  3. 逐步將 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 只在確實需要的地方局部使用。

最後,還是把相依性畫在白板上整理最快
最後,還是把相依性畫在白板上整理最快

相依性管理沒有唯一正解,但方向很明確。

盡可能讓相依性清楚呈現,隱藏相依性的工具則只在必要處謹慎使用。只要做到這點,維護就會輕鬆許多。

延伸閱讀