Service Locatorは、サービスの集中レジストリを提供するアーキテクチャパターンです。クライアントコードは、直接作成したりコンストラクター経由で受け取ったりせず、静的ロケーターを介してサービスを要求します。Service LocatorはしばしばDependency Injectionの代替と見なされます。実装は簡単ですが、依存関係を隠蔽し、テストを複雑にします。このパターンは、サービスの登録と解決を行うグローバルなSingletonクラスを通じて実装されます。詳細は — Martin FowlerによるDIとService Locatorの比較をご覧ください。
重要なポイント
Service Locatorは、サービスの作成と提供を集中化するパターンです。サービスのレジストリを含むSingletonクラス(Locator)に基づいています。キーがサービスタイプ(または識別子)、値が具体的な実装である辞書です。クライアントコードはServiceLocator.resolve(ServiceProtocol.self)を呼び出し、準備済みのインスタンスを受け取ります。このパターンはサービスの作成方法(ファクトリ、DIコンテナ、ロケーター内のnew)を規定しません。
パターン構造 — レジストリ([String: Any]型の辞書)、ロケーター(registerとresolveを持つ静的クラス)、サービス(登録されるサービス)。レジストリはファクトリ(オブジェクト作成用のクロージャ/ラムダ)または準備済みインスタンスを格納できます。ロケーターはグローバル(アプリケーションに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 — 静的プロパティとスレッドセーフな辞書による実装。ObjectIdentifier(Protocol.self)をキーとし、ファクトリ(() -> Any)を値とするレジストリが使用されます。遅延初期化(lazy var)は標準的なプラクティスです:サービスは最初の要求時に作成されます。Swiftはresolve時に明示的な型キャストを必要とします:guard let service = locator.resolve(ServiceProtocol.self) else { return }。
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 — 型安全性のためのinline reified関数を備えたobject(Singleton)によるコンパクトな実装。Kotlinはデリゲートを使用した簡潔なロケーターを可能にします:val service by locator。Reified generics()はObjectIdentifierを置き換えます — 型はジェネリックから取得されます。Kotlinロケーターは、明示的なロックなしでスレッドセーフにするためにConcurrentHashMapをよく使用します。
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のコンパイルを待つ必要がありません。欠点:テストでロケーターのオーバーライドを忘れると、テストが本番サービスを使用してしまいます。
依存関係の明示性 — 主な違い。DIは依存関係を明示的に宣言します:init(service: ServiceProtocol) — どのIDEでもクラスの依存関係が表示されます。Service Locatorはそれらを隠蔽します:dependencies = ServiceLocator.resolve() — メソッド内部に隠れています。DIでは、すべてのクラス依存関係をすぐに確認できます。Service Locatorでは、クラス本体全体を読む必要があります。これにより、Service Locatorのコードは予測可能性が低くなります:レジストリを変更すると、ロケーターを使用するすべてのクラスが壊れる可能性があります。
| 特性 | Service Locator | Dependency Injection |
|---|---|---|
| 依存関係の可視性 | メソッド本体内に隠蔽 | コンストラクターで明示 |
| テスト | グローバルレジストリの設定 | コンストラクターにモック |
| モジュール性 | グローバルレジストリ — モジュール化されていない | 別々のコンテナを持つモジュール |
| 複雑さ | シンプルな実装、50-100行 | Dagger/Swinjectが必要 |
| タイムトゥマーケット | 迅速な開始 | コンテナ設定が必要 |
Service Locatorが正当化される場合 — プロトタイプとMVP(設定不要で迅速な開始)。DIフレームワークの追加が不可能なレガシープロジェクト(複雑なビルド、リンターの制約)。インストルメンテーションライブラリ(ロギング、クラッシュレポート) — これらはすでにグローバルです。3人以上の開発者がいるプロダクションアプリケーションでは、DIが推奨されます:明示的な依存関係はリファクタリング中のエラー数を減らし、新しい開発者のオンボーディングを簡素化します。
隠れた依存関係 — メソッド内でServiceLocator.resolve()を使用するクラスは静的に分析できません。IDEは依存関係を表示せず、コンパイラはサービスが登録されているか確認しません。「Service not registered」エラーは実行時にのみ発生します。リファクタリングは危険になります:レジストリからサービスを削除すると、アプリケーション内の任意のクラスが壊れる可能性があります。DIはコンパイル時チェック(Dagger)または明示的なコンストラクターを通じてこの問題を解決します。
テストの問題 — 各テストはすべての依存関係でServiceLocator.sharedを設定する必要があります。テスト後 — 状態をリセット。並列テスト実行中、ServiceLocator.sharedのグローバル状態は競合状態を引き起こします:あるテストがモックを登録し、別のテストが他人のモックを受け取ります。解決策:スコープ付きロケーター(テストごとに1つ)またはThreadLocal。DIは最初からこの問題を解決します:各テストがモック依存関係で独自のインスタンスを作成します。
// 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は正当化され得ます。判断はコンテキストに依存します:チームでのプロダクションアプリケーションにはDI、プロトタイプの単独開発者にはService Locator。
DIコンテナ(Dagger, Swinject)は自動的にオブジェクトに依存関係を注入します — オブジェクトはコンテナの存在を知りません。Service Locator — オブジェクト自体がレジストリから依存関係を要求します。DIコンテナはIoCの原則に従います。Service Locatorはそれに違反します:オブジェクトが自身の依存関係の取得を管理します。DIコンテナはオブジェクト作成前(コンストラクター経由)に機能し、Service Locatorはコード内の任意の場所で機能します。
Service LocatorはiOSで以下の場合に正当化されます:グローバルサービス(Analytics, Logger, Crashlytics)、Swinjectの設定が過剰なプロトタイプ、大規模なレガシーモジュールの単体テスト。新しいiOSプロジェクトには、Swinjectまたはコンストラクター経由の手動DIが推奨されます。@Environmentを使用したSwiftUIもDIの一種であり、Service Locatorを回避します。
スレッドセーフなコレクションを使用します(SwiftのNSLock、KotlinのConcurrentHashMap)。テスト用 — ThreadLocalまたはスコープ付きロケーター。代替案:非同期ローカルストレージ — サービスがコルーチン/アクターにバインドされます。最良の解決策は、並列テストにService Locatorを避け、各テストに明示的なオブジェクト作成を行うDIを使用することです。
Service Locatorは通常Singletonとして実装されますが、必須ではありません。モジュール用のロケーターインスタンス(機能スコープ付きロケーター)を作成し、コンストラクター経由で渡すことができます。機能スコープ付きロケーターはグローバル状態の問題を解決しますが、隠れた依存関係の問題は解決しません。このパターンはAmbient ContextまたはScoped Locatorと呼ばれます。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。