Service Locator — パターンの本質、集中レジストリとDI

著者: IT Sectr 公開日: 2026-02-18 読了時間: 9 分

Service Locatorは、サービスの集中レジストリを提供するアーキテクチャパターンです。クライアントコードは、直接作成したりコンストラクター経由で受け取ったりせず、静的ロケーターを介してサービスを要求します。Service LocatorはしばしばDependency Injectionの代替と見なされます。実装は簡単ですが、依存関係を隠蔽し、テストを複雑にします。このパターンは、サービスの登録と解決を行うグローバルなSingletonクラスを通じて実装されます。詳細は — Martin FowlerによるDIとService Locatorの比較をご覧ください。

重要なポイント

  • Service Locator — サービスを取得するための集中レジストリ
  • DIの代替 — 実装は簡単だが、クライアントから依存関係を隠蔽する
  • アンチパターン — 多くの開発者は、隠れた依存関係のためにService Locatorをアンチパターンと見なす
  • グローバルアクセス — ロケーターはコンストラクター経由で渡さずに、どこからでも静的にアクセス可能
  • テスト — DIより複雑:各テストにグローバルロケーターの設定が必要

Service Locatorとは:パターンの本質と構造

Service Locatorは、サービスの作成と提供を集中化するパターンです。サービスのレジストリを含むSingletonクラス(Locator)に基づいています。キーがサービスタイプ(または識別子)、値が具体的な実装である辞書です。クライアントコードはServiceLocator.resolve(ServiceProtocol.self)を呼び出し、準備済みのインスタンスを受け取ります。このパターンはサービスの作成方法(ファクトリ、DIコンテナ、ロケーター内のnew)を規定しません。

パターン構造 — レジストリ([String: Any]型の辞書)、ロケーター(registerresolveを持つ静的クラス)、サービス(登録されるサービス)。レジストリはファクトリ(オブジェクト作成用のクロージャ/ラムダ)または準備済みインスタンスを格納できます。ロケーターはグローバル(アプリケーションに1つ)またはスコープ付き(機能/モジュールごと)にできます。サービスの解決は、タイプによる辞書のルックアップです。SwiftとKotlinはメタタイプを介してタイプをキーとして使用します:ObjectIdentifier(ServiceProtocol.self)

コンポーネント責任Swift/Kotlin
ServiceLocatorサービスへのグローバルアクセスclass ServiceLocator
Registryファクトリ/インスタンスの保存[ObjectIdentifier: Any]
Service具体的な実装NetworkService()

パターンの歴史 — Service LocatorはJava Patterns(1998)の書籍で説明され、後にCore J2EE Patterns(2001)でも説明されました。Martin Fowlerの2004年の記事では、Service LocatorをDIと比較し、Service Locatorは「よりシンプルな代替だが、テストには劣る」と述べています。モバイル開発では、DaggerとSwinjectが登場する前に、初期のAndroidプロジェクトとiOSアプリでService Locatorが使用されていました。現在、このパターンはレガシープロジェクトやプロトタイプでよく見られます。

SwiftのService Locator:グローバルサービスレジストリ

SwiftのService Locator — 静的プロパティとスレッドセーフな辞書による実装。ObjectIdentifier(Protocol.self)をキーとし、ファクトリ(() -> Any)を値とするレジストリが使用されます。遅延初期化(lazy var)は標準的なプラクティスです:サービスは最初の要求時に作成されます。Swiftはresolve時に明示的な型キャストを必要とします:guard let service = locator.resolve(ServiceProtocol.self) else { return }

swift
final class ServiceLocator {
    static let shared = ServiceLocator()
    private var registry: [ObjectIdentifier: Any] = [:]
    private let lock = NSLock()

    func register<T>(_ type: T.Type, factory: @escaping () -> T) {
        lock.lock()
        registry[ObjectIdentifier(type)] = factory
        lock.unlock()
    }

    func resolve<T>(_ type: T.Type) -> T {
        lock.lock()
        defer { lock.unlock() }
        guard let factory = registry[ObjectIdentifier(type)] as? () -> T else {
            fatalError("Service \(type) not registered")
        }
        return factory()
    }

    func reset() {
        lock.lock()
        registry.removeAll()
        lock.unlock()
    }
}

// 登録
ServiceLocator.shared.register(NetworkServiceProtocol.self) { NetworkService() }

// 使用
let network = ServiceLocator.shared.resolve(NetworkServiceProtocol.self)

スコープ管理 — ロケーターはファクトリ(transient — 毎回新しいオブジェクト)または準備済みインスタンス(singleton)を格納できます。ファクトリの場合、各resolveで呼び出されるクロージャが登録されます。Singletonの場合、作成されたインスタンスをキャプチャするクロージャが使用されます。スコープの追加:.transient.singleton.weak(弱参照 — 誰かが参照を保持している間、オブジェクトは存続)。UIKitのUIViewControllerでpop/dismiss時のリークを防ぐためにWeakスコープが便利です。

KotlinのService Locator:遅延登録

KotlinのService Locator — 型安全性のためのinline reified関数を備えたobject(Singleton)によるコンパクトな実装。Kotlinはデリゲートを使用した簡潔なロケーターを可能にします:val service by locator()。Reified generics()はObjectIdentifierを置き換えます — 型はジェネリックから取得されます。Kotlinロケーターは、明示的なロックなしでスレッドセーフにするためにConcurrentHashMapをよく使用します。

kotlin
object ServiceLocator {
    private val registry = ConcurrentHashMap<Class<*>, () -> Any>()

    inline fun <reified T: Any> register(noinline factory: () -> T) {
        registry[T::class.java] = factory
    }

    @Suppress("UNCHECKED_CAST")
    inline fun <reified T: Any> resolve(): T {
        val factory = registry[T::class.java]
            ?: throw IllegalStateException("Service ${T::class.simpleName} not registered")
        return factory() as T
    }

    fun clear() {
        registry.clear()
    }
}

// 登録
ServiceLocator.register<ApiService> { RetrofitApiService() }

// クラスでの使用
class UserRepository {
    private val api: ApiService = ServiceLocator.resolve()
    private val db: Database = ServiceLocator.resolve()
}

// 遅延解決のためのデリゲート
class LocatorDelegate<reified T: Any> : Lazy<T> {
    override val value: T get() = ServiceLocator.<T>resolve()
    override fun isInitialized(): Boolean = true
}

inline fun <reified T: Any> locator(): Lazy<T> = LocatorDelegate()
// 使用例: val api by locator()

AndroidのService Locator — Dagger以前の古いプロジェクトで見られます。Jetpack HiltとKoinはAndroidコミュニティでService Locatorを置き換えました。ただし、ロケーターは単体テストに関連性を保っています:Hiltなしのモックサービスを使用したシンプルなServiceLocatorスタブ。利点:テストのためにDaggerのコンパイルを待つ必要がありません。欠点:テストでロケーターのオーバーライドを忘れると、テストが本番サービスを使用してしまいます。

Service Locator vs DI:比較とどちらを選ぶべきか

依存関係の明示性 — 主な違い。DIは依存関係を明示的に宣言します:init(service: ServiceProtocol) — どのIDEでもクラスの依存関係が表示されます。Service Locatorはそれらを隠蔽します:dependencies = ServiceLocator.resolve() — メソッド内部に隠れています。DIでは、すべてのクラス依存関係をすぐに確認できます。Service Locatorでは、クラス本体全体を読む必要があります。これにより、Service Locatorのコードは予測可能性が低くなります:レジストリを変更すると、ロケーターを使用するすべてのクラスが壊れる可能性があります。

特性Service LocatorDependency Injection
依存関係の可視性メソッド本体内に隠蔽コンストラクターで明示
テストグローバルレジストリの設定コンストラクターにモック
モジュール性グローバルレジストリ — モジュール化されていない別々のコンテナを持つモジュール
複雑さシンプルな実装、50-100行Dagger/Swinjectが必要
タイムトゥマーケット迅速な開始コンテナ設定が必要

Service Locatorが正当化される場合 — プロトタイプとMVP(設定不要で迅速な開始)。DIフレームワークの追加が不可能なレガシープロジェクト(複雑なビルド、リンターの制約)。インストルメンテーションライブラリ(ロギング、クラッシュレポート) — これらはすでにグローバルです。3人以上の開発者がいるプロダクションアプリケーションでは、DIが推奨されます:明示的な依存関係はリファクタリング中のエラー数を減らし、新しい開発者のオンボーディングを簡素化します。

Service Locatorの問題点と代替案

隠れた依存関係 — メソッド内でServiceLocator.resolve()を使用するクラスは静的に分析できません。IDEは依存関係を表示せず、コンパイラはサービスが登録されているか確認しません。「Service not registered」エラーは実行時にのみ発生します。リファクタリングは危険になります:レジストリからサービスを削除すると、アプリケーション内の任意のクラスが壊れる可能性があります。DIはコンパイル時チェック(Dagger)または明示的なコンストラクターを通じてこの問題を解決します。

テストの問題 — 各テストはすべての依存関係でServiceLocator.sharedを設定する必要があります。テスト後 — 状態をリセット。並列テスト実行中、ServiceLocator.sharedのグローバル状態は競合状態を引き起こします:あるテストがモックを登録し、別のテストが他人のモックを受け取ります。解決策:スコープ付きロケーター(テストごとに1つ)またはThreadLocal。DIは最初からこの問題を解決します:各テストがモック依存関係で独自のインスタンスを作成します。

swift
// Service Locatorのテスト問題
class LoginViewModelTests: XCTestCase {
    override func setUp() {
        super.setUp()
        // テスト用グローバルレジストリの設定
        ServiceLocator.shared.register(AuthServiceProtocol.self) { MockAuthService() }
    }

    override func tearDown() {
        ServiceLocator.shared.reset()
        super.tearDown()
    }

    func testLogin() {
        let viewModel = LoginViewModel() // 内部的にServiceLocatorを使用
        // テスト...
    }
}

Service Locatorの代替案 — DI(Dagger, Swinject)、Factory Method、Ambient Context。Factory Method — グローバル状態なしのシンプルなパターン:ファクトリクラスがサービスを作成し、コンストラクターを介して渡されます。Ambient Context — 横断的関心事(ロギング、認可)のためのスレッドセーフな代替。最良の代替案は、DIフレームワークなしの手動ファクトリを使用したConstructor Injectionです:コンストラクター受け渡しによるファクトリでの依存関係の明示的な作成は、Dagger/Swinjectの設定複雑性なしでDIの明確さを提供します。

よくある質問

Service Locatorはアンチパターンですか?

多くの開発者は、Service Locatorが依存関係を隠蔽し、テストを複雑にし、クラス間の隠れた結合を生み出すため、アンチパターンと見なしています。ただし、プロトタイプ、小規模プロジェクト、およびグローバルサービス(ロギング、分析)の場合、Service Locatorは正当化され得ます。判断はコンテキストに依存します:チームでのプロダクションアプリケーションにはDI、プロトタイプの単独開発者にはService Locator。

Service LocatorとDIコンテナの違いは何ですか?

DIコンテナ(Dagger, Swinject)は自動的にオブジェクトに依存関係を注入します — オブジェクトはコンテナの存在を知りません。Service Locator — オブジェクト自体がレジストリから依存関係を要求します。DIコンテナはIoCの原則に従います。Service Locatorはそれに違反します:オブジェクトが自身の依存関係の取得を管理します。DIコンテナはオブジェクト作成前(コンストラクター経由)に機能し、Service Locatorはコード内の任意の場所で機能します。

iOSでService Locatorを使用するのはいつですか?

Service LocatorはiOSで以下の場合に正当化されます:グローバルサービス(Analytics, Logger, Crashlytics)、Swinjectの設定が過剰なプロトタイプ、大規模なレガシーモジュールの単体テスト。新しいiOSプロジェクトには、Swinjectまたはコンストラクター経由の手動DIが推奨されます。@Environmentを使用したSwiftUIもDIの一種であり、Service Locatorを回避します。

Service Locatorで競合状態を回避するには?

スレッドセーフなコレクションを使用します(SwiftのNSLock、KotlinのConcurrentHashMap)。テスト用 — ThreadLocalまたはスコープ付きロケーター。代替案:非同期ローカルストレージ — サービスがコルーチン/アクターにバインドされます。最良の解決策は、並列テストにService Locatorを避け、各テストに明示的なオブジェクト作成を行うDIを使用することです。

Service Locatorはシングルトンですか?

Service Locatorは通常Singletonとして実装されますが、必須ではありません。モジュール用のロケーターインスタンス(機能スコープ付きロケーター)を作成し、コンストラクター経由で渡すことができます。機能スコープ付きロケーターはグローバル状態の問題を解決しますが、隠れた依存関係の問題は解決しません。このパターンはAmbient ContextまたはScoped Locatorと呼ばれます。

まとめ

  • Service Locator — 静的アクセスを介してサービスを取得するための集中レジストリ
  • 隠れた依存関係 — 主な欠点:依存関係がクラスシグネチャに表示されない
  • テスト — グローバル状態とリセットの必要性によりDIより複雑
  • Swift/Kotlin — スレッドセーフな辞書とreified genericsによる実装
  • 代替案 — DI(Dagger Hilt, Swinject)— プロダクションプロジェクトの標準
  • 正当化される — プロトタイプ、グローバルサービス、レガシープロジェクトで

ターンキー方式のモバイルアプリケーションを開発します

IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。

プロジェクトについて相談

こちらもお読みください