ソフトウェア設計

Swift Service Locatorパターン:DIの代替かアンチパターンか(実務まとめ)

iOSアプリが大きくなると、依存性の管理に頭を悩ませることが一度はあるでしょう。

読了 5 分
Swift Service Locatorパターン:DIの代替かアンチパターンか(実務まとめ)のカバー画像

iOSアプリが大きくなると、依存性の管理に頭を悩ませることが一度はあるでしょう。

そうすると必ず出会うのが、Swift Service Locatorパターンです。便利だという人もいれば、アンチパターンだとして避ける人もいます。

私は全面的に導入するより、本当に必要な箇所だけに限定して使うほうがうまくいきました。

先に結論を述べると、こうです。

Service LocatorはDI(Dependency Injection、依存性注入)の完全な代替ではなく、誤って使うと依存性を隠してしまうアンチパターンになります。あくまで補助的なツールに近いものです。

今日は、このパターンとは何か、なぜ批判されるのか、それでもいつ使えるのかを、私の経験をもとに説明します。


Service Locatorパターンとは?

一言でいうと、必要なオブジェクトを中央の登録場所から取り出して使う方法です。

どこかに「登録所」を1つ用意し、オブジェクトが必要になるたびにそこから探して使います。

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 }
}

登録はアプリの起動時に一度だけ行います。

実際に使う側では、必要なものを次のように取り出します。

// ビューモデルの内部で依存性を直接「探して」取得する
final class FeedViewModel {
    private let api: APIClient = ServiceLocator.shared.resolve()
    // をコンストラクタに api渡さなくても動作する
}

ご覧のとおり、最大の利点はコンストラクタがすっきりすることです。


なぜService Locatorはアンチパターンなのか?

最大の理由は1つだけです。依存性が隠れてしまうことです。

上のFeedViewModelコードをもう一度見てみましょう。コンストラクタだけでは、このクラスがAPIClientを使うことが分かりません。

実装を開いて初めて分かります。共同作業では、これが意外に大きな不便になります。

2つ目の問題はテストです。

Constructor Injectionなら、テスト時にモックを渡すだけで済みます。一方Service Locatorではグローバルな登録場所の状態を変更する必要があり、テスト同士が状態の影響を受けやすくなります。

3つ目は実行時のリスクです。

登録を忘れた依存性を取り出そうとすると、コンパイル時ではなくアプリの実行中にクラッシュします。上の例の強制キャストas!がまさにその箇所です。

まとめると、こうなります。

比較項目 Constructor Injection(DI) Service Locator
依存性の公開 明確に分かる 内部に隠れる
テストのしやすさ 高い やや低い
エラーの検出時点 コンパイル時 実行時
コンストラクタの簡潔さ 引数が増える すっきりする
コンストラクタだけでは何を使うのか分からない点が、いつも気になっていました
コンストラクタだけでは何を使うのか分からない点が、いつも気になっていました

では、いつ使ってもよいのでしょうか?

無条件に悪いわけではありません。私は次のような状況で役立てました。

  1. アプリ全体で共有する単一のサービス(ロガー、分析ツールなど)
  2. Constructor Injectionの経路が深く、引数を何度も渡す必要がある場合
  3. レガシーコードへDIを段階的に導入する過渡期

特に3つ目は現実的です。

既存コード全体にConstructor Injectionを一度に導入するのは負担が大きいものです。そんなときはService Locatorで一時的な橋を架け、少しずつConstructor Injectionへ移行するほうが安全でした。

最近は、純粋なService LocatorよりもSwinjectやSwiftの@EnvironmentのようなDIコンテナをよく使います。

これらも内部では登録場所から取り出す構造ですが、登録の検証やスコープ管理がより堅牢なため、リスクを減らせます。


よくある質問

Q. Singletonとは何が違いますか?

Singletonは特定の1つのオブジェクトをグローバルにするものです。一方Service Locatorは複数のオブジェクトを入れておく「倉庫」です。役割は少し異なります。

Q. SwiftUIでも使いますか?

SwiftUIの@Environment@EnvironmentObjectは、Service Locatorとかなり似た概念です。Appleがフレームワークレベルで同様の方法を提供しているとも言えます。

Q.結局、何を基本にすべきですか?

基本はConstructor Injectionにすることをおすすめします。Service Locatorは本当に必要な箇所だけで局所的に使いましょう。

結局、ホワイトボードに依存性を描いて整理するのが一番早かったです
結局、ホワイトボードに依存性を描いて整理するのが一番早かったです

依存性管理に唯一の正解はありませんが、進むべき方向は明確です。

依存性はできるだけ見えるようにし、隠すツールは必要な場所だけで慎重に使いましょう。それだけでも保守はずっと楽になります。

あわせて読みたい