مقدمة: ما هو حقن التبعيات ولماذا هو مطلوب؟
\n\nتخيل أنك تقوم بتجميع جهاز كمبيوتر. بدلاً من شراء جهاز متكامل جاهز، تختار بشكل منفصل معالجًا وبطاقة رسوميات وذاكرة وصول عشوائي وتقوم بتوصيلها باللوحة الأم. إذا أردت ترقية بطاقة الرسوميات، لن تضطر إلى تغيير الكمبيوتر بأكمله - بل ستستبدل مكونًا واحدًا فقط. حقن التبعيات (DI) في البرمجة يعمل بنفس المبدأ.
\n\nحقن التبعيات (Dependency Injection) هو نمط تصميم يحصل فيه الكائن على تبعياته من الخارج، بدلاً من إنشائها بنفسه. في سياق JavaScript و Node.js، هذا يعني أن الفئة أو الدالة لا تقوم بإنشاء نسخ من الفئات الأخرى داخلها، بل تستقبلها عبر المُنشئ أو المعاملات.
\n\nهذا النهج هو جزء من مبدأ عكس التبعيات (Dependency Inversion Principle) من SOLID ويجعل الكود أكثر مرونة وقابلية للاختبار والصيانة. في هذه المقالة، سنشرح DI بأمثلة عملية للمطورين المبتدئين.
\n\nالمشكلة: الاقتران القوي للكود
\n\nلننظر إلى حالة نموذجية في تطبيق Node.js. لنفترض أن لدينا خدمة لإرسال إشعارات البريد الإلكتروني:
\n\n// بدون حقن التبعيات\nclass EmailService {\n sendEmail(to, subject, body) {\n console.log(`إرسال بريد إلكتروني إلى ${to}: ${subject}`);\n // منطق الإرسال الفعلي\n }\n}\n\nclass UserService {\n constructor() {\n this.emailService = new EmailService(); // ❌ اقتران قوي\n }\n\n registerUser(email) {\n // منطق تسجيل المستخدم\n this.emailService.sendEmail(email, 'مرحبًا بك!', '...');\n }\n}\n\nconst userService = new UserService();\nuserService.registerUser('user@example.com');\n\n\nما هي المشكلة؟ الفئة UserService مرتبطة بقوة بتطبيق معين لـ EmailService. إذا أردت:
- \n
- استبدال إرسال البريد الإلكتروني بإرسال رسائل نصية (SMS) \n
- إضافة تسجيل لجميع الرسائل المرسلة \n
- اختبار
UserServiceدون إرسال رسائل حقيقية \n
سيتعين عليك إعادة كتابة كود UserService. هذا ينتهك مبدأ الانفتاح/الإغلاق (Open/Closed Principle).
الحل: إدخال التبعيات عبر المُنشئ
\n\nالطريقة الأولى والأبسط لإدخال التبعيات هي تمريرها عبر مُنشئ الفئة. دعنا نعيد كتابة مثالنا:
\n\n// مع حقن التبعيات\nclass EmailService {\n sendEmail(to, subject, body) {\n console.log(`إرسال بريد إلكتروني إلى ${to}: ${subject}`);\n }\n}\n\nclass SmsService {\n sendSms(phone, message) {\n console.log(`إرسال رسالة نصية إلى ${phone}: ${message}`);\n }\n}\n\nclass UserService {\n constructor(notificationService) {\n this.notificationService = notificationService; // ✅ إدخال التبعية\n }\n\n registerUser(contact, isPhone = false) {\n // منطق التسجيل\n if (isPhone) {\n this.notificationService.sendSms(contact, 'مرحبًا بك!');\n } else {\n this.notificationService.sendEmail(contact, 'مرحبًا بك!', '...');\n }\n }\n}\n\n// الاستخدام\nconst emailService = new EmailService();\nconst userServiceWithEmail = new UserService(emailService);\nuserServiceWithEmail.registerUser('user@example.com');\n\nconst smsService = new SmsService();\nconst userServiceWithSms = new UserService(smsService);\nuserServiceWithSms.registerUser('+1234567890', true);\n\n\nالآن UserService لا يعرف أي خدمة محددة تُستخدم - بريد إلكتروني أم رسالة نصية. هو فقط يستدعي الدالة sendEmail أو sendSms على الكائن المُمرر. هذا يُسمى عكس التبعيات: الوحدة عالية المستوى لا تعتمد على الوحدة منخفضة المستوى، كلاهما يعتمدان على التجريد.
مزايا هذا النهج:
\n- \n
- المرونة: سهولة الاستبدال