Dependency Injection في TypeScript: للمبتدئين
\n\nفي عالم تطوير البرمجيات، غالبًا ما يُستخدم مصطلح Dependency Injection (DI)، أو حقن التبعيات. بالنسبة للمطور المبتدئ في TypeScript، قد يبدو هذا مخيفًا، لكنه في الواقع نمط بسيط وقوي يجعل الكود الخاص بك أنظف، وأكثر قابلية للاختبار، وأسهل في الصيانة. في هذه المقالة، سنشرح ما هو DI، ولماذا هو مطلوب، وكيفية تطبيقه في TypeScript عمليًا.
\n\nما هو Dependency Injection؟
\n\nتخيل أنك تقوم بتجميع جهاز كمبيوتر. بدلاً من دمج المعالج مباشرة في اللوحة الأم في المصنع، تترك فتحة يمكنك إدخال أي معالج متوافق فيها. يعمل Dependency Injection بنفس المبدأ: بدلاً من أن يقوم الفصل (class) بإنشاء تبعياته بنفسه (مثل قاعدة بيانات أو عميل HTTP)، فإنه يستقبلها من الخارج.
\n\nبعبارات بسيطة، DI هو عندما لا يقوم الكائن بإنشاء "مساعديه" داخليًا، بل يستقبلهم جاهزين عبر المُنشئ (constructor) أو طريقة (method).
\n\nالمشكلة بدون DI: الاقتران الشديد (Tight Coupling)
\n\nلننظر إلى مثال شائع في المشاريع الحقيقية:
\n\nclass UserService {\n private database: MySQLDatabase;\n\n constructor() {\n this.database = new MySQLDatabase(); // اقتران شديد!\n }\n\n getUser(id: number) {\n return this.database.findUserById(id);\n }\n}\n\nهنا، UserService مرتبط بشدة بـ MySQLDatabase. إذا أردت التبديل إلى PostgreSQL أو كتابة اختبارات وحدة (unit tests) باستخدام قاعدة بيانات وهمية، فسيتعين عليك إعادة كتابة الفصل بأكمله. هذا ينتهك مبدأ Open-Closed من SOLID.
الحل باستخدام DI: المرونة وقابلية الاختبار
\n\nالآن، دعنا نعيد كتابة نفس الفصل باستخدام DI:
\n\ninterface Database {\n findUserById(id: number): User | null;\n}\n\nclass UserService {\n constructor(private database: Database) {} // التبعية تُمرر من الخارج\n\n getUser(id: number) {\n return this.database.findUserById(id);\n }\n}\n\n// الاستخدام\nconst mySQL = new MySQLDatabase();\nconst service = new UserService(mySQL); // حقن التبعية\n\nالآن، UserService لا يعرف أي قاعدة بيانات يعمل معها. يمكنك بسهولة استبدال MySQLDatabase بـ PostgreSQLDatabase أو MockDatabase للاختبارات.
الطرق الأساسية لحقن التبعيات
\n\nفي TypeScript، هناك ثلاث طرق رئيسية لحقن التبعيات. دعنا نستعرض كلًا منها.
\n\n1. الحقن عبر المُنشئ (Constructor Injection)
\n\nهذه هي الطريقة الأكثر شيوعًا والموصى بها. يتم تمرير التبعيات عبر مُنشئ الفصل.
\n\nclass Logger {\n log(message: string) {\n console.log(`[LOG]: ${message}`);\n }\n}\n\nclass OrderService {\n constructor(private logger: Logger) {}\n\n createOrder(data: any) {\n this.logger.log('Order created');\n // منطق إنشاء الطلب\n }\n}\n\nconst logger = new Logger();\nconst orderService = new OrderService(logger);\n\n2. الحقن عبر الخصائص (Property Injection)
\n\nيتم تعيين التبعية مباشرة إلى خاصية الكائن بعد إنشائه. يُستخدم بشكل أقل شيوعًا لأنه قد يؤدي إلى تهيئة غير كاملة للكائن.
\n\nclass EmailService {\n sendEmail(to: string, message: string) {\n console.log(`Email sent to ${to}: ${message}`);\n }\n}\n\nclass NotificationService {\n emailService!: EmailService; // تأكيد النوع\n\n notifyUser(user: string) {\n this.emailService.sendEmail(user, 'Hello!');\n }\n}\n\nconst notification = new NotificationService();\nnotification.emailService = new EmailService(); // تعيين الخاصية\nnotification.notifyUser('user@example.com');\n\n