Service Locator — ماهیت الگو، ثبت مرکزی و DI

نویسنده: IT Sectr منتشر شده: 2026-02-18 زمان مطالعه: 9 دقیقه

Service Locator — الگوی معماری که یک ثبت مرکزی (registry) از سرویس‌ها ارائه می‌دهد. کد کلاینت سرویس را از طریق یک مکان‌یاب ایستا درخواست می‌کند، بدون اینکه آن را مستقیماً ایجاد کند یا از طریق سازنده دریافت نماید. Service Locator اغلب به عنوان جایگزینی برای Dependency Injection در نظر گرفته می‌شود: پیاده‌سازی آن ساده‌تر است، اما وابستگی‌ها را پنهان می‌کند و آزمایش را دشوارتر می‌سازد. این الگو از طریق یک کلاس Singleton سراسری با ثبت و حل سرویس‌ها پیاده‌سازی می‌شود. بیشتر — در مقایسه DI و Service Locator از مارتین فاولر.

نکات کلیدی

  • Service Locator — ثبت مرکزی (registry) برای دریافت سرویس‌ها
  • جایگزین DI — پیاده‌سازی ساده‌تر، اما وابستگی‌ها را از کلاینت پنهان می‌کند
  • ضدالگو — بسیاری از توسعه‌دهندگان Service Locator را به دلیل وابستگی‌های پنهان ضدالگو می‌دانند
  • دسترسی سراسری — مکان‌یاب به صورت ایستا از هر مکانی در دسترس است، بدون ارسال از طریق سازنده
  • آزمایش — دشوارتر از 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: ثبت سراسری سرویس‌ها

Service Locator در Swift — پیاده‌سازی از طریق ویژگی‌های ایستا و یک فرهنگ لغت ایمن در برابر نخ. از یک ثبت با 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) را ذخیره کند. برای کارخانه‌ها یک closure ثبت می‌شود که در هر resolve فراخوانی می‌شود. برای singleton — closure با گرفتن نمونه ایجاد شده. افزودن محدوده: .transient، .singleton، .weak (مرجع ضعیف — شیء تا زمانی که کسی به آن مرجع دارد زنده می‌ماند). محدوده weak برای UIKit ViewController مناسب است تا از نشت حافظه در pop/dismiss جلوگیری شود.

Service Locator در Kotlin: ثبت تنبل

Service Locator در Kotlin — پیاده‌سازی فشرده از طریق object (Singleton) با توابع درون‌خطی reified برای ایمنی نوع. Kotlin امکان ایجاد یک مکان‌یاب مختصر را فراهم می‌کند: val service by locator<ServiceProtocol>() با نماینده، که کد را تمیزتر می‌کند. Reified generics (<reified T>) جایگزین 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()

Service Locator در Android — در پروژه‌های قدیمی قبل از ظهور Dagger یافت می‌شود. Jetpack Hilt و Koin Service Locator را در جامعه Android کنار زده‌اند. با این حال، مکان‌یاب برای تست‌های واحد همچنان مرتبط است: یک تقلید ساده از ServiceLocator با سرویس‌های ساختگی بدون Hilt. نکته مثبت: نیازی به انتظار برای کامپایل Dagger برای تست‌ها نیست. نکته منفی: اگر فراموش کنید مکان‌یاب را در تست بازنویسی کنید، تست‌ها از سرویس‌های تولیدی استفاده می‌کنند.

Service Locator در مقابل DI: مقایسه و زمان انتخاب هرکدام

آشکار بودن وابستگی‌ها — تفاوت اصلی. DI وابستگی‌ها را به صورت آشکار اعلام می‌کند: init(service: ServiceProtocol) — هر IDE وابستگی‌های کلاس را نشان می‌دهد. Service Locator آنها را پنهان می‌کند: dependencies = ServiceLocator.resolve() — درون متد پنهان است. با DI می‌توان تمام وابستگی‌های کلاس را فوراً دید، با Service Locator باید کل بدنه کلاس را خواند. این کار کد مبتنی بر Service Locator را کمتر قابل پیش‌بینی می‌کند: تغییر در ثبت می‌تواند هر کلاسی را که از مکان‌یاب استفاده می‌کند، خراب کند.

ویژگیService LocatorDependency Injection
قابلیت مشاهده وابستگی‌هاپنهان در بدنه متدهاآشکار در سازنده
آزمایشپیکربندی ثبت سراسریMock در سازنده
ماژولاریتیثبت سراسری — ماژولار نیستماژول‌ها با کانتینرهای مختلف
پیچیدگیپیاده‌سازی ساده، 50-100 خطنیاز به Dagger/Swinject
زمان عرضه به بازارشروع سریعپیکربندی کانتینر

زمانی که Service Locator موجه است — نمونه‌های اولیه و MVP (شروع سریع بدون پیکربندی). پروژه‌های قدیمی که نمی‌توان چارچوب DI را به آنها اضافه کرد (کامپایل پیچیده، محدودیت‌های لینتر). کتابخانه‌های ابزاری (ثبت رویداد، گزارش خرابی) — آنها به هر حال سراسری هستند. برای برنامه‌های تولیدی با تیم ۳ توسعه‌دهنده یا بیشتر، DI توصیه می‌شود: وابستگی‌های آشکار تعداد خطاها را در بازسازی کاهش داده و ورود توسعه‌دهندگان جدید به پروژه را آسان‌تر می‌کند.

مشکلات Service Locator و جایگزین‌ها

وابستگی‌های پنهان — کلاسی که از ServiceLocator.resolve() درون متد استفاده می‌کند، نمی‌توان به صورت ایستا تحلیل کرد. IDE وابستگی‌ها را نشان نمی‌دهد، کامپایلر بررسی نمی‌کند که آیا سرویس ثبت شده است یا خیر. خطای «Service not registered» فقط در زمان اجرا رخ می‌دهد. بازسازی کد خطرناک می‌شود: حذف یک سرویس از ثبت می‌تواند هر کلاسی را در برنامه خراب کند. DI این مشکل را از طریق بررسی‌های زمان کامپایل (Dagger) یا سازنده‌های آشکار حل می‌کند.

مشکل آزمایش — هر تست باید ServiceLocator.shared را با تمام وابستگی‌ها پیکربندی کند. بعد از تست — بازنشانی (reset()) وضعیت. در اجرای موازی تست‌ها، وضعیت سراسری ServiceLocator.shared منجر به شرایط مسابقه می‌شود: یک تست یک mock را ثبت می‌کند، دیگری — mock شخص دیگری را دریافت می‌کند. راه‌حل: مکان‌یاب‌های محدوده‌ای (یکی به ازای هر تست) یا 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 — جایگزین ایمن در برابر نخ برای دغدغه‌های متقاطع (ثبت رویداد، احراز هویت). بهترین جایگزین — Constructor Injection با کارخانه دستی بدون چارچوب DI: ایجاد آشکار وابستگی‌ها در کارخانه با ارسال از طریق سازنده، شفافیت DI را بدون پیچیدگی پیکربندی Dagger/Swinject فراهم می‌کند.

سوالات متداول

آیا Service Locator یک ضدالگو است؟

بسیاری از توسعه‌دهندگان Service Locator را ضدالگو می‌دانند، زیرا وابستگی‌ها را پنهان می‌کند، آزمایش را دشوارتر می‌کند و ارتباطات پنهانی بین کلاس‌ها ایجاد می‌کند. با این حال، در نمونه‌های اولیه، پروژه‌های کوچک و برای سرویس‌های سراسری (ثبت رویداد، تحلیل) Service Locator می‌تواند موجه باشد. تصمیم به زمینه بستگی دارد: برای برنامه تولیدی با تیم — DI، برای یک توسعه‌دهنده روی نمونه اولیه — Service Locator.

تفاوت Service Locator با کانتینر DI چیست؟

کانتینر DI (Dagger, Swinject) وابستگی‌ها را به طور خودکار به شیء تزریق می‌کند — شیء از وجود کانتینر آگاه نیست. Service Locator — شیء خودش وابستگی‌ها را از ثبت درخواست می‌کند. کانتینر DI از اصل IoC پیروی می‌کند، Service Locator — آن را نقض می‌کند: شیء خودش دریافت وابستگی‌هایش را مدیریت می‌کند. کانتینر DI قبل از ایجاد شیء (از طریق سازنده) کار می‌کند، Service Locator — در هر جای کد.

چه زمانی از Service Locator در iOS استفاده کنیم؟

Service Locator در iOS برای موارد زیر موجه است: سرویس‌های سراسری (Analytics, Logger, Crashlytics)، نمونه‌های اولیه که پیکربندی Swinject در آنها بیش از حد است، و برای تست‌های واحد ماژول‌های قدیمی بزرگ. برای پروژه‌های جدید iOS، Swinject یا DI دستی از طریق سازنده توصیه می‌شود. SwiftUI با Environment — همچنین شکلی از DI است که از Service Locator اجتناب می‌کند.

چگونه از شرایط مسابقه در Service Locator جلوگیری کنیم؟

از مجموعه ایمن در برابر نخ استفاده کنید (NSLock در Swift، ConcurrentHashMap در Kotlin). برای تست‌ها — ThreadLocal یا مکان‌یاب محدوده‌ای. جایگزین: ذخیره‌سازی محلی ناهمزمان — سرویس‌ها به کروتین/اکتور متصل هستند. بهترین راه‌حل — اجتناب از Service Locator برای تست‌های موازی و استفاده از DI با ایجاد آشکار اشیاء برای هر تست.

آیا Service Locator یک Singleton است؟

معمولاً Service Locator به عنوان Singleton پیاده‌سازی می‌شود، اما این اجباری نیست. می‌توان یک نمونه مکان‌یاب برای ماژول (feature-scoped locator) ایجاد کرد و آن را از طریق سازنده ارسال نمود. مکان‌یاب محدوده‌ای (feature-scoped) مشکل وضعیت سراسری را حل می‌کند، اما مشکل وابستگی‌های پنهان را حل نمی‌کند. چنین الگویی Ambient Context یا Scoped Locator نامیده می‌شود.

خلاصه

  • Service Locator — ثبت مرکزی برای دریافت سرویس‌ها از طریق دسترسی ایستا
  • وابستگی‌های پنهان — نقص اصلی: وابستگی‌ها در امضای کلاس قابل مشاهده نیستند
  • آزمایش — دشوارتر از DI به دلیل وضعیت سراسری و نیاز به بازنشانی
  • Swift/Kotlin — پیاده‌سازی از طریق فرهنگ لغت ایمن در برابر نخ و reified generics
  • جایگزین — DI (Dagger Hilt, Swinject) — استاندارد برای پروژه‌های تولیدی
  • موجه — در نمونه‌های اولیه، برای سرویس‌های سراسری و پروژه‌های قدیمی

ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد

IT Sectr از سال 2017 برنامه‌های iOS و Android را برای استارتاپ‌ها و کسب‌وکارها ایجاد می‌کند. ما به شما مشاوره می‌دهیم و بهترین راه‌حل را پیشنهاد خواهیم کرد.

بحث درباره پروژه

همچنین بخوانید