مقدمة إلى Dependency Injection في Kotlin
في التطوير الحديث باستخدام Kotlin، خاصة عند إنشاء تطبيقات Android، أصبح مبدأ Dependency Injection (DI) ليس مجرد ممارسة جيدة، بل ضرورة. يتيح حقن التبعيات كتابة كود نظيف وقابل للاختبار والتوسع، مما يقلل من الترابط بين مكونات النظام.
في هذه المقالة، سنشرح ما هو Dependency Injection، وما المشكلات التي يحلها، وكيفية تنفيذ DI في Kotlin - بدءًا من النهج اليدوي وصولاً إلى استخدام الأطر الشائعة مثل Hilt و Koin.
ما هو Dependency Injection (حقن التبعيات)؟
Dependency Injection هو نمط تصميم يحصل فيه الكائن على تبعياته من الخارج، بدلاً من إنشائها بنفسه. بدلاً من كتابة val engine = Engine() داخل الفئة، نقوم "بحقن" نسخة جاهزة عبر المُنشئ أو الطريقة أو الحقل.
لننظر إلى مثال بسيط بدون DI:
class Car { private val engine = Engine() // ارتباط صلب
fun start() { engine.start() }}المشكلة: الفئة Car مرتبطة بشكل صلب بتنفيذ محدد لـ Engine. يصعب اختبارها وتوسيعها.
الآن مثال مع DI عبر المُنشئ:
class Car(private val engine: Engine) { // التبعية تُمرر من الخارج
fun start() { engine.start() }}
// الاستخدامval engine = Engine()val car = Car(engine)car.start()الآن Car لا يعرف كيف يتم إنشاء Engine. يمكننا بسهولة استبداله بـ MockEngine للاختبارات أو بتنفيذ آخر.
لماذا DI مهم في Kotlin؟
Kotlin، كلغة حديثة، يوفر إمكانيات ممتازة لـ DI:
- Null Safety - يساعد Kotlin في تجنب NullPointerException عند العمل مع التبعيات.
- بناء جملة موجز - المُنشئات ذات المعاملات تجعل DI طبيعياً.
- التوافق مع Java - جميع أطر DI الشائعة (Dagger, Hilt) تدعم Kotlin.
- الأسلوب الوظيفي - يسمح Kotlin باستخدام lambdas والمفوّضين لتنفيذ DI.
طرق تنفيذ DI في Kotlin
1. الحقن اليدوي (Manual DI)
أبسط طريقة هي تمرير التبعيات عبر المُنشئ. بالنسبة للمشاريع الصغيرة، غالباً ما يكون هذا كافياً.
class Database(private val connection: Connection)
class UserRepository(private val database: Database)
class UserViewModel(private val userRepository: UserRepository)
// تجميع التبعيات يدوياًfun main() { val connection = Connection("postgres://localhost:5432") val database = Database(connection) val repository = UserRepository(database) val viewModel = UserViewModel(repository)}العيب: مع نمو المشروع، تصبح الإدارة اليدوية مرهقة.
2. Service Locator (نمط معاكس أو حل مؤقت)
أحياناً يُستخدم Service Locator - كائن مركزي يخزن جميع التبعيات. لكنه يُعتبر نمطاً معاكساً لأنه يخفي التبعيات الحقيقية للفئة.
object ServiceLocator { private val database = Database() private val repository = UserRepository(database) fun provideViewModel() = UserViewModel(repository)}
// الاستخدامval viewModel = ServiceLocator.provideViewModel()3. أطر DI: Hilt و Koin
للمشاريع الجادة، من الأفضل استخدام حلول جاهزة. دعنا ننظر إلى الأكثر شيوعاً لـ Kotlin/Android.
Hilt (مبني على Dagger)
Hilt هي مكتبة DI الرسمية من Google لنظام Android. وهي مبنية على Dagger ولكن مع واجهة برمجة تطبيقات مبسطة.
// 1. التعليق التوضيحي @HiltAndroidApp في Application@HiltAndroidAppclass MyApplication : Application(