مقدمة: لماذا يحتاج كودك إلى Dependency Injection؟
كل مطور JavaScript عاجلاً أم آجلاً يواجه مشكلة الكود "المقترن بإحكام" (tightly coupled). تخيل كلاس UserService الذي ينشئ داخلياً نسخة من Database و Logger. ماذا سيحدث عندما تريد اختبار UserService بدون قاعدة بيانات حقيقية؟ أو استبدال Logger بتنفيذ آخر؟ في أفضل الأحوال - ستعيد كتابة الكلاس بأكمله، وفي أسوأها - ستعطل التطبيق بأكمله.
حل هذه المشكلة هو Dependency Injection (DI)، أو حقن التبعيات. إنه أحد مبادئ التصميم الرئيسية التي تقوم عليها فكرة Inversion of Control (IoC). في هذه المقالة، سنشرح ما هو DI، وكيفية تنفيذه في JavaScript الخالص (Node.js) بدون استخدام أطر العمل، ولماذا هو مهم جداً لبناء تطبيقات قابلة للتوسع والاختبار.
ما هو Dependency Injection بلغة بسيطة؟
تخيل أنك تبني منزلاً. بدلاً من تثبيت المقابس في الجدران بشكل دائم، تقوم بتركيب موصلات قياسية. اليوم توصل بها مصباحاً قديماً، وغداً ثريا ذكية. أنت لا تغير الجدران - أنت تغير التبعيات.
في البرمجة، Dependency Injection هو نمط (pattern) يحصل فيه الكائن على تبعياته (كائنات أخرى، خدمات، إعدادات) من الخارج، بدلاً من إنشائها بنفسه. بدلاً من كتابة:
// ❌ سيء: ارتباط قويclass UserService { constructor() { this.db = new Database('mongodb://...'); this.logger = new ConsoleLogger(); }}نكتب:
// ✅ جيد: حقن التبعياتclass UserService { constructor(database, logger) { this.db = database; this.logger = logger; }}الآن UserService لا يعرف كيف يتم إنشاء Database و Logger. مهمته فقط استخدامها. وهذا هو عكس التحكم: السيطرة على إنشاء التبعيات تنتقل من الكلاس إلى الكود المستدعي.
أنواع حقن التبعيات
هناك ثلاث طرق رئيسية للحقن:
- عبر المُنشئ (Constructor Injection) - الطريقة الأكثر شيوعاً والمفضلة. يتم تمرير التبعيات عند إنشاء الكائن.
- عبر الخصائص (Property Injection) - يتم تعيين التبعيات بعد إنشاء الكائن. مناسب للتبعيات الاختيارية.
- عبر الطرق (Method Injection) - يتم تمرير التبعية إلى طريقة معينة عندما تكون مطلوبة فقط لإجراء واحد.
كتابة حاوية DI بسيطة على Node.js
في المشاريع الكبيرة، يصبح إدارة التبعيات يدوياً أمراً صعباً. هنا تأتي حاوية DI - كائن يعرف كيفية إنشاء وتخزين جميع تبعيات تطبيقك. دعنا نكتب حاوية بسيطة ولكنها عملية باستخدام JavaScript الخالص.
حاويتنا ستدعم وضعين: singleton (نسخة واحدة للتطبيق بأكمله) و transient (نسخة جديدة عند كل طلب).
// di-container.jsclass DIContainer { constructor() { this.registry = new Map(); this.instances = new Map(); }
// تسجيل التبعية مع دالة المصنع register(name, factory, { singleton = true } = {}) { this.registry.set(name, { factory, singleton }); if (singleton) { this.instances.delete(name); // إعادة تعيين الذاكرة المؤقتة عند إعادة التسجيل } }
// الحصول على نسخة التبعية resolve(name) { const item = this.registry.get(name); if (!item) { throw new Error(`التبعية "${name}" غير موجودة`); }