حقن التبعيات (DI) هي تقنية يحصل فيها الكائن على تبعياته من الخارج بدلاً من إنشائها بنفسه. DI هي تطبيق لمبدأ IoC (عكس التحكم) وتشكل الأساس لـ Dagger و Hilt و Swinject. حقن التبعيات يقلل من اقتران الكود، ويبسط الاختبار، ويجعل البنية مرنة. في Android، DI هو المعيار عبر Dagger Hilt من Google، وفي iOS عبر Swinject أو الحقن اليدوي. تعرف على المزيد في دليل DI لنظام Android.
الخلاصة
حقن التبعيات هي تقنية يحصل فيها الكائن على التبعيات (الخدمات، المستودعات، الإعدادات) من خلال المُنشئ أو المُحدد أو الواجهة، بدلاً من إنشائها بنفسه باستخدام new. الهدف من DI هو تقليل الاقتران بين الفئات. إذا قامت فئة بإنشاء التبعيات بنفسها، فإنها ترتبط بشدة بتطبيقات محددة، مما يعقد الاختبار والتعديل. مع DI، تعمل الفئة مع تجريد (بروتوكول/واجهة)، ويتم توفير التطبيق الملموس من الخارج.
ثلاث طرق للحقن — حقن المُنشئ (عبر init/constructor)، حقن المُحدد (عبر الخاصية/setter)، حقن الواجهة (عبر طريقة واجهة). حقن المُنشئ هو الطريقة المفضلة: التبعيات مرئية بوضوح في التوقيع، ويتم إنشاء الكائن دائمًا في حالة صالحة. يستخدم حقن المُحدد للتبعيات الاختيارية بقيمة افتراضية. حقن الواجهة نادر، ويستخدم بشكل أساسي لحاويات DI.
| نوع DI | الطريقة | متى تستخدم | مثال |
|---|---|---|---|
| المُنشئ | معاملات المُهيئ | التبعيات الإلزامية | init(service: ServiceProtocol) |
| الخاصية | خاصية الفئة | التبعيات الاختيارية | var service: ServiceProtocol? |
| الطريقة | معامل الطريقة | التبعيات المؤقتة | func doWork(with service: Service) |
حاوية DI — مكتبة تدير إنشاء التبعيات ودورة حياتها. تحتوي الحاوية على تسجيلات الأنواع (كل نوع تجريدي مُقابل بتطبيق ملموس) ومصنع لإنشاء الكائنات مع التبعيات المُحلَّلة. في Android — Dagger/Hilt، في iOS — Swinject، Needle، Dip. يمكن للحاوية إدارة النطاق: singleton (مثيل واحد لكل تطبيق)، نطاق الميزة (لكل شاشة) أو كائن جديد عند كل طلب.
Dagger Hilt هو غلاف حول Dagger من Google، مكتبة DI القياسية لنظام Android. يعمل Hilt على تبسيط Dagger: يزيل الإنشاء اليدوي للمكونات، ويضيف @HiltAndroidApp و @AndroidEntryPoint و @Module. يتكامل Hilt مع دورة حياة Android: ViewModel و Activity و Fragment و Service و BroadcastReceiver يمكنهم تلقي التبعيات من خلال التعليقات التوضيحية. يحدث توليد الكود في وقت التجميع — يقوم Dagger بتوليد تطبيقات المكونات، مما يعطي عبء runtime صفري.
// فئة التطبيق
@HiltAndroidApp
class MyApp : Application()
// الوحدة — تحدد كيفية إنشاء التبعيات
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient = OkHttpClient.Builder().build()
@Provides
@Singleton
fun provideApiService(client: OkHttpClient): ApiService {
return Retrofit.Builder()
.baseUrl("https://api.example.com")
.client(client)
.build()
.create(ApiService::class.java)
}
}
// ViewModel يتلقى التبعية عبر المُنشئ
@HiltViewModel
class MainViewModel @Inject constructor(
private val apiService: ApiService
) : ViewModel() {
private val _state = MutableStateFlow(MainState.Loading)
val state: StateFlow<MainState> = _state.asStateFlow()
fun loadData() {
viewModelScope.launch {
_state.value = MainState.Success(apiService.getData())
}
}
}
// Activity — @AndroidEntryPoint يفعل DI
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels()
}
مكونات Dagger ونطاقات الرؤية — @Singleton (للتطبيق بالكامل)، @ActivityScoped (لـ Activity)، @FragmentScoped (لـ Fragment)، @ViewModelScoped (لـ ViewModel). يحدد اختيار النطاق عمر الكائن. @Singleton — مثيل واحد لكل عملية، مناسب لـ OkHttpClient وقواعد البيانات. @ActivityScoped — الكائن يعيش طالما يعيش Activity، للتبعيات على مستوى الشاشة. @ViewModelScoped — جديد في Hilt 2.45+، الكائن يعيش طالما يعيش ViewModel، مناسب لنطاقات coroutine.
Swinject هو إطار DI مفتوح المصدر شائع لنظام iOS. يوفر Swinject Container و Assemblies ونطاقات مختلفة. على عكس Dagger، يعمل Swinject في وقت التشغيل — يتم حل التبعيات ديناميكيًا دون توليد كود. هذا يجعل Swinject أسهل في الإعداد، لكن التصحيح أكثر صعوبة: خطأ التبعية غير المحلولة يظهر فقط في وقت التشغيل. يدعم Swinject حقن المُنشئ وحقن الخاصية وحقن الطريقة.
import Swinject
// Assembly — مجموعة تسجيلات
class NetworkAssembly: Assembly {
func assemble(container: Container) {
container.register(NetworkServiceProtocol.self) { _ in
NetworkService()
}.inObjectScope(.container) // singleton
container.register(UserRepositoryProtocol.self) { r in
UserRepository(
networkService: r.resolve(NetworkServiceProtocol.self)!
)
}
}
}
// ViewModel عبر حقن المُنشئ
class ProfileViewModel: ObservableObject {
private let repository: UserRepositoryProtocol
init(repository: UserRepositoryProtocol) {
self.repository = repository
}
@Published var user: User?
func loadUser() {
repository.fetchUser { [weak self] user in
self?.user = user
}
}
}
// إعداد DI في AppDelegate أو التطبيق
let assembler = Assembler([NetworkAssembly(), ServiceAssembly()])
let viewModel = assembler.resolver.resolve(ProfileViewModel.self)!
// حقن الخاصية لـ UIKit ViewController
container.register(UserViewController.self) { r in
let vc = UserViewController()
vc.viewModel = r.resolve(ProfileViewModel.self)
return vc
}
نطاقات Swinject — .transient (كائن جديد في كل مرة)، .container (singleton لكل حاوية)، .graph (افتراضي — تتم مشاركة الكائن داخل رسم بياني واحد للتبعيات). لتطبيقات iOS، .container و .transient كافية. يدعم Swinject أيضًا Assembler — تجميع Assembly لبنية معيارية. للاختبار، يتم استبدال Assembly بـ MockAssembly، مما يتيح استبدال التبعيات دون تغيير كود الإنتاج.
DI مقابل Service Locator — كلا النمطين يحلان مشكلة إدارة التبعيات، ولكن بشكل مختلف. DI يحقن التبعيات في الكائن؛ Service Locator يوفر سجلاً عالمياً يطلب منه الكائن التبعيات بنفسه. DI يعلن التبعيات بوضوح من خلال المُنشئ (أو المُحدد). Service Locator يخفي التبعيات — يتم طلبها داخل الطريقة، مما يجعل التوقيع أقل إفادة. DI أسهل في الاختبار: يكفي تمرير mock إلى المُنشئ. Service Locator يتطلب إعداد السجل العالمي لكل اختبار.
| الخاصية | حقن التبعيات | Service Locator | الحقن اليدوي |
|---|---|---|---|
| وضوح التبعيات | في المُنشئ | مخفية في جسم الطريقة | واضحة |
| الاختبار | Mock في المُنشئ | إعداد Locator | Mock في المُنشئ |
| تعقيد الإعداد | يتطلب حاوية DI | سجل عالمي | إنشاء يدوي |
| عبء runtime | Dagger — وقت التجميع | بحث في runtime | لا يوجد |
DI مقابل الحقن اليدوي — بدون حاوية DI، يتم إنشاء التبعيات يدويًا في المصانع أو AppDelegate. لـ 5-10 فئات، الحقن اليدوي أبسط — لا يتطلب تعلم Dagger أو Swinject. لـ 50+ فئة، يصبح الحقن اليدوي مشكلة: مُنشئات بها 5-6 معاملات، ترتيب إنشاء معقد، تكرار الكود. حاوية DI تؤتمت هذه العمليات وتوفر إدارة واضحة لدورة الحياة. الحقن اليدوي بدون حاوية هو خيار جيد للمشاريع الصغيرة والنماذج الأولية.
حقن المُنشئ — المعيار. استخدم دائمًا حقن المُنشئ للتبعيات الإلزامية. هذا يجعل التبعيات واضحة والكائن جاهزًا دائمًا للعمل. حقن المُحدد — فقط للتبعيات الاختيارية (مثل delegate أو listener). حقن الواجهة — لا تستخدمه إلا إذا كنت تكتب مكتبة DI خاصة بك. حقن المُنشئ هو الطريقة الوحيدة لضمان إنشاء الكائن في حالة صالحة.
فئة واحدة — مسؤولية واحدة. إذا كان مُنشئ الفئة يتطلب 5+ معاملات، فمن المحتمل أن الفئة تنتهك مبدأ المسؤولية الفردية. قسم الفئة إلى عدة فئات بعدد أقل من التبعيات. علامة: إذا كنت تكتب فئة ServiceManager بها 6 خدمات مختلفة — فهذا هو النمط المضاد God Object. استخرج منطق الأعمال إلى Use Cases (Interactors)، كل منها مع 1-2 تبعيات.
// ❌ سيء: 6 تبعيات — God Object
class ProfileViewModel @Inject constructor(
private val api: ApiService,
private val db: Database,
private val analytics: Analytics,
private val prefs: Preferences,
private val location: LocationProvider,
private val notification: NotificationManager
)
// ✅ جيد: Use Cases مع 1-2 تبعيات
class ProfileViewModel @Inject constructor(
private val loadProfileUseCase: LoadProfileUseCase,
private val trackAnalyticsUseCase: TrackAnalyticsUseCase
)
النطاق ودورة الحياة — اختر النطاق الصحيح لكل تبعية. Singletons: OkHttpClient، قاعدة البيانات، SharedPreferences. نطاق الميزة: المستودعات، Use Cases (إذا كانت بلا حالة). Transient: Value Objects، DateFormatter، المُحلِّلات. أخطاء النطاق مشكلة شائعة: singleton يخزن حالة الشاشة يؤدي إلى تسربات. في Android، @ActivityScoped من Hilt يحل هذه المشكلة؛ في Swinject، استخدم .container بحذر.
الأسئلة الشائعة
new ينشئ اقتراناً قوياً بين الفئات — لا يمكنك تغيير التطبيق دون تعديل الكود. يصبح الاختبار صعباً: لا يمكنك حقن mock بدلاً من خدمة حقيقية. يتم انتهاك SRP: الفئة مسؤولة عن كل من منطق الأعمال وإنشاء التبعيات. DI يحل هذه المشكلات عن طريق حقن التبعيات من الخارج والعمل مع التجريدات.
Dagger Hilt هو المعيار من Google، DI في وقت التجميع مع توليد الكود، أداء أفضل وتكامل مع Jetpack. Koin هو DI في وقت التشغيل، أسهل في الإعداد، لكنه أبطأ ومع أخطاء في runtime. اختر Hilt للمشاريع الإنتاجية. Koin مناسب للنماذج الأولية والتطبيقات الصغيرة.
لا. لنظام iOS تتوفر: Swinject (runtime، شائع)، Needle (وقت التجميع من Uber)، Dip (خفيف)، Weaver (قائم على Sourcery). Apple لا توفر حاوية DI مدمجة، لكن الحقن اليدوي عبر init هو ممارسة قياسية. لـ SwiftUI، غالباً ما يكون DI اليدوي عبر Environment أو @StateObject دون مكتبات خارجية كافياً.
نعم. الحقن اليدوي عبر المُنشئ هو DI بدون إطار عمل. Service Locator هو بديل بدون إطار عمل. المصانع و Factory Method هي أيضاً أشكال من DI. إطار العمل (Dagger، Swinject) يؤتمت التسجيل الروتيني وحل التبعيات، لكن لـ 10-20 فئة، DI اليدوي كافٍ.
DI هو تقنية (قالب) تطبق مبدأ عكس التحكم. على عكس أنماط GoF، DI ليس له بنية صارمة من 3-4 فئات. DI هو طريقة لتنظيم التبعيات، وليس نمط تصميم. حاويات DI (Dagger، Swinject) هي أطر عمل تؤتمت هذه التقنية.
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا