Service Locator — الگوی معماری که یک ثبت مرکزی (registry) از سرویسها ارائه میدهد. کد کلاینت سرویس را از طریق یک مکانیاب ایستا درخواست میکند، بدون اینکه آن را مستقیماً ایجاد کند یا از طریق سازنده دریافت نماید. Service Locator اغلب به عنوان جایگزینی برای Dependency Injection در نظر گرفته میشود: پیادهسازی آن سادهتر است، اما وابستگیها را پنهان میکند و آزمایش را دشوارتر میسازد. این الگو از طریق یک کلاس Singleton سراسری با ثبت و حل سرویسها پیادهسازی میشود. بیشتر — در مقایسه DI و Service Locator از مارتین فاولر.
نکات کلیدی
Service Locator — الگویی که ایجاد و ارائه سرویسها را متمرکز میکند. در پایه یک کلاس Singleton (Locator) قرار دارد که شامل یک ثبت (Registry) از سرویسهاست: یک فرهنگ لغت که کلید آن نوع (یا شناسه) سرویس و مقدار آن پیادهسازی مشخص است. کد کلاینت ServiceLocator.resolve(ServiceProtocol.self) را فراخوانی کرده و یک نمونه آماده دریافت میکند. الگو تعیین نمیکند که سرویس چگونه ایجاد شود — کارخانه، کانتینر DI یا new در داخل مکانیاب.
ساختار الگو — Registry (ثبت از نوع [String: Any])، Locator (کلاس ایستا با register و resolve)، Service (سرویس قابل ثبت). ثبت میتواند کارخانهها (closure/lambda برای ایجاد شیء) یا نمونههای آماده را ذخیره کند. مکانیاب میتواند سراسری (یکی برای کل برنامه) یا محدودهای (به ازای ویژگی/ماژول) باشد. حل سرویس — جستجو در فرهنگ لغت بر اساس نوع. Swift و Kotlin از نوع به عنوان کلید از طریق متاتایپها استفاده میکنند: ObjectIdentifier(ServiceProtocol.self).
| جزء | مسئولیت | Swift/Kotlin |
|---|---|---|
| ServiceLocator | دسترسی سراسری به سرویسها | class ServiceLocator |
| Registry | ذخیرهسازی کارخانهها/نمونهها | [ObjectIdentifier: Any] |
| Service | پیادهسازی مشخص | NetworkService() |
تاریخچه الگو — Service Locator در کتاب Java Patterns (1998) و بعداً در Core J2EE Patterns (2001) توصیف شد. مارتین فاولر در مقالهای در سال 2004 Service Locator را با DI مقایسه میکند و اشاره میکند که Service Locator «جایگزین سادهتری است، اما برای آزمایش بدتر است». در توسعه موبایل، Service Locator در پروژههای اولیه Android و برنامههای iOS قبل از ظهور Dagger و Swinject استفاده میشد. اکنون این الگو بیشتر در پروژههای قدیمی و نمونههای اولیه یافت میشود.
Service Locator در Swift — پیادهسازی از طریق ویژگیهای ایستا و یک فرهنگ لغت ایمن در برابر نخ. از یک ثبت با 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) را ذخیره کند. برای کارخانهها یک closure ثبت میشود که در هر resolve فراخوانی میشود. برای singleton — closure با گرفتن نمونه ایجاد شده. افزودن محدوده: .transient، .singleton، .weak (مرجع ضعیف — شیء تا زمانی که کسی به آن مرجع دارد زنده میماند). محدوده weak برای UIKit ViewController مناسب است تا از نشت حافظه در pop/dismiss جلوگیری شود.
Service Locator در Kotlin — پیادهسازی فشرده از طریق object (Singleton) با توابع درونخطی reified برای ایمنی نوع. Kotlin امکان ایجاد یک مکانیاب مختصر را فراهم میکند: val service by locator<ServiceProtocol>() با نماینده، که کد را تمیزتر میکند. Reified generics (<reified T>) جایگزین 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()
Service Locator در Android — در پروژههای قدیمی قبل از ظهور Dagger یافت میشود. Jetpack Hilt و Koin Service Locator را در جامعه Android کنار زدهاند. با این حال، مکانیاب برای تستهای واحد همچنان مرتبط است: یک تقلید ساده از ServiceLocator با سرویسهای ساختگی بدون Hilt. نکته مثبت: نیازی به انتظار برای کامپایل Dagger برای تستها نیست. نکته منفی: اگر فراموش کنید مکانیاب را در تست بازنویسی کنید، تستها از سرویسهای تولیدی استفاده میکنند.
آشکار بودن وابستگیها — تفاوت اصلی. DI وابستگیها را به صورت آشکار اعلام میکند: init(service: ServiceProtocol) — هر IDE وابستگیهای کلاس را نشان میدهد. Service Locator آنها را پنهان میکند: dependencies = ServiceLocator.resolve() — درون متد پنهان است. با DI میتوان تمام وابستگیهای کلاس را فوراً دید، با Service Locator باید کل بدنه کلاس را خواند. این کار کد مبتنی بر Service Locator را کمتر قابل پیشبینی میکند: تغییر در ثبت میتواند هر کلاسی را که از مکانیاب استفاده میکند، خراب کند.
| ویژگی | Service Locator | Dependency Injection |
|---|---|---|
| قابلیت مشاهده وابستگیها | پنهان در بدنه متدها | آشکار در سازنده |
| آزمایش | پیکربندی ثبت سراسری | Mock در سازنده |
| ماژولاریتی | ثبت سراسری — ماژولار نیست | ماژولها با کانتینرهای مختلف |
| پیچیدگی | پیادهسازی ساده، 50-100 خط | نیاز به Dagger/Swinject |
| زمان عرضه به بازار | شروع سریع | پیکربندی کانتینر |
زمانی که Service Locator موجه است — نمونههای اولیه و MVP (شروع سریع بدون پیکربندی). پروژههای قدیمی که نمیتوان چارچوب DI را به آنها اضافه کرد (کامپایل پیچیده، محدودیتهای لینتر). کتابخانههای ابزاری (ثبت رویداد، گزارش خرابی) — آنها به هر حال سراسری هستند. برای برنامههای تولیدی با تیم ۳ توسعهدهنده یا بیشتر، DI توصیه میشود: وابستگیهای آشکار تعداد خطاها را در بازسازی کاهش داده و ورود توسعهدهندگان جدید به پروژه را آسانتر میکند.
وابستگیهای پنهان — کلاسی که از ServiceLocator.resolve() درون متد استفاده میکند، نمیتوان به صورت ایستا تحلیل کرد. IDE وابستگیها را نشان نمیدهد، کامپایلر بررسی نمیکند که آیا سرویس ثبت شده است یا خیر. خطای «Service not registered» فقط در زمان اجرا رخ میدهد. بازسازی کد خطرناک میشود: حذف یک سرویس از ثبت میتواند هر کلاسی را در برنامه خراب کند. DI این مشکل را از طریق بررسیهای زمان کامپایل (Dagger) یا سازندههای آشکار حل میکند.
مشکل آزمایش — هر تست باید ServiceLocator.shared را با تمام وابستگیها پیکربندی کند. بعد از تست — بازنشانی (reset()) وضعیت. در اجرای موازی تستها، وضعیت سراسری ServiceLocator.shared منجر به شرایط مسابقه میشود: یک تست یک mock را ثبت میکند، دیگری — mock شخص دیگری را دریافت میکند. راهحل: مکانیابهای محدودهای (یکی به ازای هر تست) یا 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 — جایگزین ایمن در برابر نخ برای دغدغههای متقاطع (ثبت رویداد، احراز هویت). بهترین جایگزین — Constructor Injection با کارخانه دستی بدون چارچوب DI: ایجاد آشکار وابستگیها در کارخانه با ارسال از طریق سازنده، شفافیت DI را بدون پیچیدگی پیکربندی Dagger/Swinject فراهم میکند.
سوالات متداول
بسیاری از توسعهدهندگان 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 دستی از طریق سازنده توصیه میشود. SwiftUI با Environment — همچنین شکلی از DI است که از Service Locator اجتناب میکند.
از مجموعه ایمن در برابر نخ استفاده کنید (NSLock در Swift، ConcurrentHashMap در Kotlin). برای تستها — ThreadLocal یا مکانیاب محدودهای. جایگزین: ذخیرهسازی محلی ناهمزمان — سرویسها به کروتین/اکتور متصل هستند. بهترین راهحل — اجتناب از Service Locator برای تستهای موازی و استفاده از DI با ایجاد آشکار اشیاء برای هر تست.
معمولاً Service Locator به عنوان Singleton پیادهسازی میشود، اما این اجباری نیست. میتوان یک نمونه مکانیاب برای ماژول (feature-scoped locator) ایجاد کرد و آن را از طریق سازنده ارسال نمود. مکانیاب محدودهای (feature-scoped) مشکل وضعیت سراسری را حل میکند، اما مشکل وابستگیهای پنهان را حل نمیکند. چنین الگویی Ambient Context یا Scoped Locator نامیده میشود.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.