Service Locator — pola arsitektur yang menyediakan registri pusat (registry) layanan. Kode klien meminta layanan melalui locator statis, tanpa membuatnya secara langsung atau menerimanya melalui konstruktor. Service Locator sering dianggap sebagai alternatif Dependency Injection: lebih sederhana dalam implementasi, tetapi menyembunyikan ketergantungan dan mempersulit pengujian. Pola ini diimplementasikan melalui kelas Singleton global dengan registrasi dan resolusi layanan. Lebih lanjut — dalam perbandingan DI dan Service Locator oleh Martin Fowler.
Poin Utama
Service Locator — pola yang memusatkan pembuatan dan penyediaan layanan. Di dasarnya adalah kelas Singleton (Locator) yang berisi registri (Registry) layanan: kamus di mana kuncinya adalah jenis (atau pengidentifikasi) layanan dan nilainya adalah implementasi konkret. Kode klien memanggil ServiceLocator.resolve(ServiceProtocol.self) dan menerima instance yang sudah jadi. Pola ini tidak menentukan bagaimana layanan dibuat — pabrik, container DI, atau new di dalam locator.
Struktur pola — Registry (registri tipe [String: Any]), Locator (kelas statis dengan register dan resolve), Service (layanan yang dapat didaftarkan). Registri dapat menyimpan pabrik (closure/lambda untuk membuat objek) atau instance yang sudah jadi. Locator dapat bersifat global (satu per aplikasi) atau terbatas lingkup (per fitur/modul). Resolusi layanan adalah pencarian dalam kamus berdasarkan jenis. Swift dan Kotlin menggunakan jenis sebagai kunci melalui metatype: ObjectIdentifier(ServiceProtocol.self).
| Komponen | Tanggung jawab | Swift/Kotlin |
|---|---|---|
| ServiceLocator | Akses global ke layanan | class ServiceLocator |
| Registry | Penyimpanan pabrik/instance | [ObjectIdentifier: Any] |
| Service | Implementasi konkret | NetworkService() |
Sejarah pola — Service Locator dijelaskan dalam buku Java Patterns (1998) dan kemudian dalam Core J2EE Patterns (2001). Martin Fowler dalam artikel tahun 2004 membandingkan Service Locator dengan DI, mencatat bahwa Service Locator adalah „alternatif yang lebih sederhana, tetapi lebih buruk untuk pengujian“. Dalam pengembangan seluler, Service Locator digunakan dalam proyek Android awal dan aplikasi iOS sebelum munculnya Dagger dan Swinject. Saat ini pola ini lebih sering ditemukan dalam proyek lama dan prototipe.
Service Locator di Swift — implementasi melalui properti statis dan kamus thread-safe. Digunakan registri dengan ObjectIdentifier(Protocol.self) sebagai kunci dan pabrik (()->Any) sebagai nilai. Inisialisasi malas (lazy var) — praktik standar: layanan dibuat saat permintaan pertama. Swift memerlukan konversi tipe eksplisit saat 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()
}
}
// Pendaftaran
ServiceLocator.shared.register(NetworkServiceProtocol.self) { NetworkService() }
// Penggunaan
let network = ServiceLocator.shared.resolve(NetworkServiceProtocol.self)
Manajemen lingkup — locator dapat menyimpan pabrik (transient — objek baru setiap kali) atau instance yang sudah jadi (singleton). Untuk pabrik, closure didaftarkan yang dipanggil pada setiap resolve. Untuk singleton — closure dengan menangkap instance yang dibuat. Menambahkan lingkup: .transient, .singleton, .weak (referensi lemah — objek hidup selama seseorang memegang referensi). Lingkup weak berguna untuk UIKit ViewController untuk menghindari kebocoran memori saat pop/dismiss.
Service Locator di Kotlin — implementasi ringkas melalui object (Singleton) dengan fungsi inline reified untuk keamanan tipe. Kotlin memungkinkan pembuatan locator yang ringkas: val service by locator<ServiceProtocol>() dengan delegasi, yang membuat kode lebih bersih. Reified generics (<reified T>) menggantikan ObjectIdentifier — tipe diperoleh dari generic. Locator Kotlin sering menggunakan ConcurrentHashMap untuk keamanan thread tanpa penguncian eksplisit.
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()
}
}
// Pendaftaran
ServiceLocator.register<ApiService> { RetrofitApiService() }
// Penggunaan di kelas
class UserRepository {
private val api: ApiService = ServiceLocator.resolve()
private val db: Database = ServiceLocator.resolve()
}
// Delegasi untuk resolusi malas
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()
// Penggunaan: val api by locator()
Service Locator di Android — ditemukan dalam proyek lama sebelum munculnya Dagger. Jetpack Hilt dan Koin telah menggantikan Service Locator di komunitas Android. Namun, locator tetap relevan untuk pengujian unit: tiruan sederhana ServiceLocator dengan layanan mock tanpa Hilt. Plus: tidak perlu menunggu kompilasi Dagger untuk pengujian. Minus: jika lupa mengganti locator dalam pengujian, pengujian menggunakan layanan produksi.
Kejelasan ketergantungan — perbedaan utama. DI mendeklarasikan ketergantungan secara eksplisit: init(service: ServiceProtocol) — IDE mana pun menunjukkan ketergantungan kelas. Service Locator menyembunyikannya: dependencies = ServiceLocator.resolve() — tersembunyi di dalam metode. Dengan DI Anda dapat langsung melihat semua ketergantungan kelas, dengan Service Locator Anda harus membaca seluruh tubuh kelas. Ini membuat kode pada Service Locator kurang dapat diprediksi: perubahan pada registri dapat merusak kelas mana pun yang menggunakan locator.
| Karakteristik | Service Locator | Dependency Injection |
|---|---|---|
| Visibilitas ketergantungan | Tersembunyi di dalam metode | Eksplisit di konstruktor |
| Pengujian | Konfigurasi registri global | Mock di konstruktor |
| Modularitas | Registri global — tidak modular | Modul dengan wadah berbeda |
| Kompleksitas | Implementasi sederhana, 50-100 baris | Memerlukan Dagger/Swinject |
| Time-to-market | Mulai cepat | Konfigurasi wadah |
Kapan Service Locator dibenarkan — prototipe dan MVP (mulai cepat tanpa konfigurasi). Proyek lama di mana kerangka DI tidak dapat ditambahkan (kompilasi kompleks, batasan linter). Pustaka instrumental (logging, pelaporan crash) — mereka sudah bersifat global. Untuk aplikasi produksi dengan tim dari 3 pengembang, DI direkomendasikan: ketergantungan eksplisit mengurangi jumlah kesalahan saat refactoring dan memudahkan pengenalan pengembang baru ke dalam proyek.
Ketergantungan tersembunyi — kelas yang menggunakan ServiceLocator.resolve() di dalam metode tidak dapat dianalisis secara statis. IDE tidak menampilkan ketergantungan, kompiler tidak memeriksa apakah layanan telah didaftarkan. Kesalahan „Service not registered“ hanya terjadi saat runtime. Refactoring menjadi berbahaya: menghapus layanan dari registri dapat merusak kelas mana pun dalam aplikasi. DI memecahkan masalah ini melalui pemeriksaan waktu kompilasi (Dagger) atau konstruktor eksplisit.
Masalah pengujian — setiap pengujian harus mengonfigurasi ServiceLocator.shared dengan semua ketergantungan. Setelah pengujian — mereset (reset()) status. Dalam eksekusi paralel pengujian, status global ServiceLocator.shared menyebabkan race condition: satu pengujian mendaftarkan mock, yang lain — menerima mock milik orang lain. Solusi: locator lingkup (satu per pengujian) atau ThreadLocal. DI memecahkan masalah ini dari akarnya: setiap pengujian membuat instance-nya sendiri dengan ketergantungan mock.
// Masalah pengujian Service Locator
class LoginViewModelTests: XCTestCase {
override func setUp() {
super.setUp()
// Konfigurasi registri global untuk pengujian
ServiceLocator.shared.register(AuthServiceProtocol.self) { MockAuthService() }
}
override func tearDown() {
ServiceLocator.shared.reset()
super.tearDown()
}
func testLogin() {
let viewModel = LoginViewModel() // menggunakan ServiceLocator di dalam
// pengujian...
}
}
Alternatif Service Locator — DI (Dagger, Swinject), Factory Method, Ambient Context. Factory Method — pola sederhana tanpa status global: kelas pabrik membuat layanan dan dikirim melalui konstruktor. Ambient Context — alternatif thread-safe untuk cross-cutting concerns (logging, otorisasi). Alternatif terbaik — Constructor Injection dengan pabrik manual tanpa kerangka DI: pembuatan ketergantungan secara eksplisit di pabrik dengan pengiriman melalui konstruktor memberikan kejelasan DI tanpa kompleksitas konfigurasi Dagger/Swinject.
Pertanyaan yang Sering Diajukan
Banyak pengembang menganggap Service Locator sebagai antipola karena menyembunyikan ketergantungan, mempersulit pengujian, dan menciptakan hubungan tersembunyi antar kelas. Namun, dalam prototipe, proyek kecil, dan untuk layanan global (logging, analitik) Service Locator dapat dibenarkan. Keputusan tergantung pada konteks: untuk aplikasi produksi dengan tim — DI, untuk satu pengembang pada prototipe — Service Locator.
Container DI (Dagger, Swinject) menyuntikkan ketergantungan ke dalam objek secara otomatis — objek tidak tahu tentang keberadaan container. Service Locator — objek sendiri meminta ketergantungan dari registri. Container DI mematuhi prinsip IoC, Service Locator — melanggarnya: objek sendiri mengelola perolehan ketergantungannya. Container DI bekerja sebelum pembuatan objek (melalui konstruktor), Service Locator — di mana saja dalam kode.
Service Locator dibenarkan di iOS untuk: layanan global (Analytics, Logger, Crashlytics), prototipe di mana konfigurasi Swinject berlebihan, dan untuk pengujian unit modul lama yang besar. Untuk proyek iOS baru, disarankan Swinject atau DI manual melalui konstruktor. SwiftUI dengan Environment — juga bentuk DI yang menghindari Service Locator.
Gunakan koleksi thread-safe (NSLock di Swift, ConcurrentHashMap di Kotlin). Untuk pengujian — ThreadLocal atau locator lingkup. Alternatif: async-local storage — layanan terikat pada coroutine/aktor. Solusi terbaik — hindari Service Locator untuk pengujian paralel dan gunakan DI dengan pembuatan objek eksplisit untuk setiap pengujian.
Biasanya Service Locator diimplementasikan sebagai Singleton, tetapi ini tidak wajib. Anda dapat membuat instance locator untuk modul (feature-scoped locator) dan mengirimkannya melalui konstruktor. Feature-scoped locator memecahkan masalah status global, tetapi tidak memecahkan masalah ketergantungan tersembunyi. Pola seperti ini disebut Ambient Context atau Scoped Locator.
Kesimpulan
Kami akan mengembangkan aplikasi seluler turnkey
IT Sectr membuat aplikasi iOS dan Android untuk startup dan bisnis sejak 2017. Kami akan memberi saran dan mengusulkan solusi terbaik.
Baca juga