Service Locator هو نمط معماري يوفر سجلاً مركزياً للخدمات. يقوم كود العميل بطلب خدمة من خلال محدد موقع ثابت، دون إنشائها مباشرة أو استلامها عبر المُنشئ. غالباً ما يُعتبر Service Locator بديلاً لـ Dependency Injection: فهو أبسط في التنفيذ، لكنه يخفي التبعيات ويعقد الاختبار. يتم تنفيذ النمط عبر فئة Singleton عامة مع تسجيل الخدمات وحلها. المزيد من التفاصيل — في مقارنة DI و Service Locator من Martin Fowler.
النقاط الرئيسية
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 — تنفيذ عبر خصائص ثابتة وقاموس آمن للخيوط. يُستخدم سجل مع 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 مناسب لـ UIViewControllers في UIKit لتجنب التسريبات عند pop/dismiss.
Service Locator في Kotlin — تنفيذ مدمج عبر object (Singleton) مع دوال inline reified لأمان الأنواع. تسمح 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()
Service Locator في Android — يوجد في المشاريع القديمة السابقة لـ Dagger. استبدل Jetpack Hilt و Koin Service Locator في مجتمع Android. ومع ذلك، يظل المحدد مناسباً للاختبارات الوحدوية: كعب ServiceLocator بسيط مع خدمات mock بدون 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 مبرراً — النماذج الأولية و MVPs (بداية سريعة دون تهيئة). المشاريع القديمة حيث لا يمكن إضافة إطار DI (بناء معقد، قيود linter). مكتبات الأدوات (تسجيل، تقارير الأعطال) — هي بالفعل عامة. لتطبيقات الإنتاج مع فريق من 3+ مطورين، يُوصى بـ DI: التبعيات الصريحة تقلل الأخطاء أثناء إعادة الهيكلة وتبسط إدخال مطورين جدد.
التبعيات المخفية — فئة تستخدم ServiceLocator.resolve() داخل دالة لا يمكن تحليلها استاتيكياً. IDE لا يُظهر التبعيات، المترجم لا يتحقق مما إذا كانت الخدمة مسجلة. خطأ "Service not registered" يحدث فقط في وقت التشغيل. تصبح إعادة الهيكلة خطيرة: إزالة خدمة من السجل قد تكسر أي فئة في التطبيق. DI يحل هذه المشكلة من خلال فحوصات وقت التجميع (Dagger) أو مُنشئات صريحة.
مشكلة الاختبار — يجب على كل اختبار تهيئة ServiceLocator.shared مع جميع التبعيات. بعد الاختبار — إعادة تعيين الحالة. أثناء تنفيذ الاختبارات المتوازية، تؤدي الحالة العامة لـ ServiceLocator.shared إلى ظروف سباق: اختبار يسجل mock، وآخر يستلم mock شخص آخر. الحل: محددات بنطاق (واحد لكل اختبار) أو ThreadLocal. DI يحل هذه المشكلة من البداية: كل اختبار ينشئ مثيله الخاص مع تبعيات mock.
// مشكلة اختبار 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 أو محدد بنطاق. بديل: تخزين async-local — خدمات مرتبطة بـ coroutine/actor. أفضل حل هو تجنب Service Locator للاختبارات المتوازية واستخدام DI مع إنشاء صريح للكائنات لكل اختبار.
عادةً ما يُنفذ Service Locator كـ Singleton، لكن هذا ليس إلزامياً. يمكنك إنشاء مثيل محدد لوحدة (محدد بنطاق ميزة) وتمريره عبر المُنشئ. محدد بنطاق ميزة يحل مشكلة الحالة العامة لكنه لا يحل مشكلة التبعيات المخفية. هذا النمط يُسمى Ambient Context أو Scoped Locator.
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.