مقدمة إلى Dependency Injection في TypeScript
Dependency Injection (DI) هو أحد أنماط التصميم الرئيسية التي تتيح إنشاء تطبيقات ضعيفة الاقتران، قابلة للاختبار، وسهلة الصيانة. في سياق TypeScript، بفضل الكتابة الصارمة (strict typing) وإمكانيات decorators، يصبح تنفيذ DI أنيقًا وآمنًا بشكل خاص.
في هذه المقالة، سننظر في ماهية Dependency Injection، وكيفية تنفيذه في TypeScript، وما هي الأدوات التي يمكنها تبسيط هذه العملية. ستتعلم كيف يساعد DI في مكافحة الاقتران القوي بين الكلاسات ويجعل كودك أكثر مرونة.
ما هو Dependency Injection ولماذا هو مطلوب؟
Dependency Injection (حقن التبعيات) هو نمط يتم فيه استلام الكائن لتبعياته من الخارج، بدلاً من إنشائها بنفسه. وهذا يحقق مبدأ Inversion of Control (IoC) - عكس التحكم.
المشكلة بدون DI:
class Logger { log(message: string): void { console.log(message); }}
class UserService { private logger: Logger;
constructor() { this.logger = new Logger(); // اقتران قوي }
createUser(name: string): void { this.logger.log(`User ${name} created`); }}في هذا المثال، UserService مرتبط بقوة بتنفيذ محدد لـ Logger. إذا أردنا استبدال الـ logger بـ، على سبيل المثال، ملف أو بعيد، فسيتعين علينا تغيير كود UserService.
الحل مع DI:
interface ILogger { log(message: string): void;}
class ConsoleLogger implements ILogger { log(message: string): void { console.log(message); }}
class FileLogger implements ILogger { log(message: string): void { // كتابة إلى ملف }}
class UserService { constructor(private logger: ILogger) {} // يتم حقن التبعية
createUser(name: string): void { this.logger.log(`User ${name} created`); }}
// الاستخدامconst service = new UserService(new ConsoleLogger());مزايا DI:
- قابلية الاختبار — من السهل استبدال التبعيات الحقيقية بأشياء mock.
- المرونة — يمكن تغيير سلوك النظام دون تغيير كود الكلاسات.
- فصل المسؤوليات — الكلاسات لا تهتم بإنشاء تبعياتها.
- إعادة الاستخدام — يمكن استخدام التبعيات في أجزاء مختلفة من التطبيق.
تنفيذ Dependency Injection يدويًا في TypeScript
أبسط طريقة هي حقن التبعيات عبر الـ constructor أو الخصائص. TypeScript مع واجهاته (interfaces) ومعدلات الوصول (access modifiers) يجعل هذا الأمر مريحًا للغاية.
الحقن عبر الـ Constructor (Constructor Injection)
interface IRepository<T> { getAll(): Promise<T[]>; getById(id: number): Promise<T | null>;}
class UserRepository implements IRepository<User> { async getAll(): Promise<User[]> { // منطق حقيقي للعمل مع قاعدة البيانات return []; }
async getById(id: number): Promise<User | null> { return null; }}
class UserController { constructor( private repository: IRepository<User>, private logger: ILogger ) {}
async getUsers(): Promise<User[]> { this.logger.log('Fetching users...'); return this.repository.getAll(); }}
// إنشاء رسم بياني للتبعياتconst logger = new ConsoleLogger();const repository = new UserRepository();const controller = new UserController(repository, logger);الحقن عبر الخصائص (Property Injection)
class EmailService { private sender!: ISender;
setSender(sender: ISender): void {