مقدمة: مشكلة الترابط الصلب للكود
\n\nكل مطور Node.js يواجه عاجلاً أم آجلاً موقفًا يؤدي فيه تغيير في وحدة واحدة إلى سلسلة من التعديلات في عشر وحدات أخرى. يتجلى هذا بشكل خاص عند كتابة اختبارات الوحدة: لاختبار خدمة واحدة، يجب تشغيل قاعدة البيانات وواجهات API الخارجية وذاكرة التخزين المؤقت. جذر المشكلة هو الترابط الصلب (tight coupling).
\n\nحقن التبعية (DI) هو نمط تصميم يحل هذه المشكلة. بدلاً من أن يقوم الفصل أو الدالة بإنشاء تبعياته بنفسه، فإنه يتلقاها من الخارج. هذا يجعل الكود مرنًا وقابلًا للاختبار ومتوافقًا مع مبدأ عكس التبعيات (Dependency Inversion Principle) من SOLID.
\n\nفي هذه المقالة، سنستعرض DI عمليًا: من الحقن اليدوي البسيط إلى استخدام الحاويات الاحترافية في Node.js.
\n\n1. الحقن اليدوي للتبعيات: الخطوة الأولى
\n\nالنهج السيء: الترابط الصلب
\nلننظر إلى مثال نموذجي لخدمة إرسال البريد الإلكتروني:
\n\n// userService.js\nconst emailService = require('./emailService');\n\nclass UserService {\n async registerUser(email) {\n // اعتماد صلب على تنفيذ محدد\n await emailService.sendWelcomeEmail(email);\n console.log('تم تسجيل المستخدم');\n }\n}\n\nmodule.exports = UserService;\n\nالمشكلة: عند اختبار UserService، لا يمكن استبدال emailService بنموذج وهمي (mock). سيتعين استخدام أدوات معقدة مثل proxyquire أو jest.mock.
النهج الجيد: الحقن عبر المُنشئ
\nلنعد كتابة الخدمة، ونمرر التبعية عبر المُنشئ:
\n\n// userService.js\nclass UserService {\n constructor(emailService) {\n this.emailService = emailService;\n }\n\n async registerUser(email) {\n await this.emailService.sendWelcomeEmail(email);\n console.log('تم تسجيل المستخدم');\n }\n}\n\nmodule.exports = UserService;\n\nالآن يمكننا اختبار الخدمة بسهولة:
\n\n// userService.test.js\nconst UserService = require('./userService');\n\nconst mockEmailService = {\n sendWelcomeEmail: jest.fn().mockResolvedValue(true)\n};\n\ntest('يجب إرسال بريد ترحيبي', async () => {\n const service = new UserService(mockEmailService);\n await service.registerUser('test@test.com');\n expect(mockEmailService.sendWelcomeEmail).toHaveBeenCalledWith('test@test.com');\n});\n\n2. أنواع حقن التبعية في JavaScript
\n\nفي JavaScript/Node.js، هناك ثلاث طرق رئيسية لحقن التبعيات:
\n\n2.1. الحقن عبر المُنشئ
\nالطريقة الأكثر شيوعًا وقابلية للتنبؤ. يتم تمرير التبعيات عند إنشاء الكائن:
\n\nclass DatabaseService {\n constructor(connection) {\n this.connection = connection;\n }\n\n async query(sql) {\n return this.connection.execute(sql);\n }\n}\n\n// الاستخدام\nconst db = new DatabaseService(new MySQLConnection());\n\n2.2. الحقن عبر الدوال الضابطة (Setters)
\nمناسب عندما يمكن تغيير التبعيات أثناء عمل الكائن:
\n\nclass LoggerService {\n setLogger(logger) {\n this.logger = logger;\n }\n\n log(message) {\n this.logger.write(`[${new Date().toISOString()}] ${message}`);\n }\n}\n\nconst logger = new LoggerService();\nlogger.setLogger(new ConsoleLogger());\nlogger.log('تم تشغيل التطبيق');\n\n2.3. الحقن عبر معاملات الدالة
\nمثالي للنهج الوظيفي والأدوات المساعدة:
\n\nfunction sendNotification(user, emailService, smsService) {\n emailService.send(user.email, 'إشعار');\n smsService.send(user.phone, 'إشعار');\n}\n\n// الاختبار\nconst mockEmail = { send: jest.fn() };\nconst mockSms = { send: jest.fn() };\nsendNotification({ email: 'a@b.com', phone: '123' }, mockEmail, mockSms);\n\n