Service Locator — جوهر النمط، السجل المركزي و DI

المؤلف: IT Sectr نُشر: 2026-02-18 وقت القراءة: 9 دق

Service Locator هو نمط معماري يوفر سجلاً مركزياً للخدمات. يقوم كود العميل بطلب خدمة من خلال محدد موقع ثابت، دون إنشائها مباشرة أو استلامها عبر المُنشئ. غالباً ما يُعتبر Service Locator بديلاً لـ Dependency Injection: فهو أبسط في التنفيذ، لكنه يخفي التبعيات ويعقد الاختبار. يتم تنفيذ النمط عبر فئة Singleton عامة مع تسجيل الخدمات وحلها. المزيد من التفاصيل — في مقارنة DI و Service Locator من Martin Fowler.

النقاط الرئيسية

  • Service Locator — سجل مركزي للحصول على الخدمات
  • بديل DI — أبسط في التنفيذ، لكنه يخفي التبعيات عن العميل
  • نمط مضاد — يعتبر العديد من المطورين Service Locator نمطاً مضاداً بسبب التبعيات المخفية
  • وصول عام — المحدد متاح ثابتاً من أي مكان، دون تمرير عبر المُنشئ
  • الاختبار — أكثر تعقيداً من DI: يتطلب تهيئة المحدد العام لكل اختبار

ما هو Service Locator: جوهر النمط وهيكله

Service Locator هو نمط يمركز إنشاء وتوفير الخدمات. يعتمد على فئة Singleton (Locator) تحتوي على سجل للخدمات: قاموس حيث المفتاح هو نوع الخدمة (أو المعرف) والقيمة هي التنفيذ الملموس. يقوم كود العميل باستدعاء ServiceLocator.resolve(ServiceProtocol.self) ويستلم مثيلاً جاهزاً. لا يحدد النمط كيفية إنشاء الخدمة — مصنع، حاوية DI أو new داخل المحدد.

هيكل النمط — السجل (قاموس من نوع [String: Any])، المحدد (فئة ثابتة تحتوي على register و resolve)، الخدمة (الخدمة التي يتم تسجيلها). يمكن للسجل تخزين المصانع (closures/lambdas لإنشاء الكائنات) أو مثيلات جاهزة. يمكن أن يكون المحدد عاماً (واحد لكل تطبيق) أو بنطاق (حسب الميزة/الوحدة). حل الخدمة هو بحث في القاموس حسب النوع. تستخدم 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 مناسب لـ UIViewControllers في UIKit لتجنب التسريبات عند pop/dismiss.

Service Locator في Kotlin: التسجيل الكسول

Service Locator في Kotlin — تنفيذ مدمج عبر object (Singleton) مع دوال inline reified لأمان الأنواع. تسمح 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()

Service Locator في Android — يوجد في المشاريع القديمة السابقة لـ Dagger. استبدل Jetpack Hilt و Koin Service Locator في مجتمع Android. ومع ذلك، يظل المحدد مناسباً للاختبارات الوحدوية: كعب ServiceLocator بسيط مع خدمات mock بدون Hilt. الإيجابي: لا حاجة لانتظار تجميع Dagger للاختبارات. السلبي: إذا نسيت تجاوز المحدد في اختبار، تستخدم الاختبارات خدمات الإنتاج.

Service Locator vs 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 مبرراً — النماذج الأولية و MVPs (بداية سريعة دون تهيئة). المشاريع القديمة حيث لا يمكن إضافة إطار DI (بناء معقد، قيود linter). مكتبات الأدوات (تسجيل، تقارير الأعطال) — هي بالفعل عامة. لتطبيقات الإنتاج مع فريق من 3+ مطورين، يُوصى بـ DI: التبعيات الصريحة تقلل الأخطاء أثناء إعادة الهيكلة وتبسط إدخال مطورين جدد.

مشاكل Service Locator والبدائل

التبعيات المخفية — فئة تستخدم ServiceLocator.resolve() داخل دالة لا يمكن تحليلها استاتيكياً. IDE لا يُظهر التبعيات، المترجم لا يتحقق مما إذا كانت الخدمة مسجلة. خطأ "Service not registered" يحدث فقط في وقت التشغيل. تصبح إعادة الهيكلة خطيرة: إزالة خدمة من السجل قد تكسر أي فئة في التطبيق. DI يحل هذه المشكلة من خلال فحوصات وقت التجميع (Dagger) أو مُنشئات صريحة.

مشكلة الاختبار — يجب على كل اختبار تهيئة ServiceLocator.shared مع جميع التبعيات. بعد الاختبار — إعادة تعيين الحالة. أثناء تنفيذ الاختبارات المتوازية، تؤدي الحالة العامة لـ ServiceLocator.shared إلى ظروف سباق: اختبار يسجل mock، وآخر يستلم mock شخص آخر. الحل: محددات بنطاق (واحد لكل اختبار) أو ThreadLocal. DI يحل هذه المشكلة من البداية: كل اختبار ينشئ مثيله الخاص مع تبعيات mock.

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 أو محدد بنطاق. بديل: تخزين async-local — خدمات مرتبطة بـ coroutine/actor. أفضل حل هو تجنب Service Locator للاختبارات المتوازية واستخدام DI مع إنشاء صريح للكائنات لكل اختبار.

هل Service Locator هو singleton؟

عادةً ما يُنفذ Service Locator كـ Singleton، لكن هذا ليس إلزامياً. يمكنك إنشاء مثيل محدد لوحدة (محدد بنطاق ميزة) وتمريره عبر المُنشئ. محدد بنطاق ميزة يحل مشكلة الحالة العامة لكنه لا يحل مشكلة التبعيات المخفية. هذا النمط يُسمى Ambient Context أو Scoped Locator.

الملخص

  • Service Locator — سجل مركزي للحصول على الخدمات عبر وصول ثابت
  • تبعيات مخفية — العيب الرئيسي: التبعيات غير مرئية في توقيع الفئة
  • الاختبار — أكثر تعقيداً من DI بسبب الحالة العامة والحاجة لإعادة التعيين
  • Swift/Kotlin — تنفيذ عبر قاموس آمن للخيوط و reified generics
  • بديل — DI (Dagger Hilt, Swinject) — المعيار لمشاريع الإنتاج
  • مبرر — في النماذج الأولية، للخدمات العامة والمشاريع القديمة

سنقوم بتطوير تطبيق جوال جاهز

تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.

مناقشة المشروع

اقرأ أيضًا