Qu'est-ce que l'Injection de Dépendances en TypeScript et pourquoi en a-t-on besoin ?
L'Injection de Dépendances (DI) est un pattern de conception qui implémente le principe d'inversion de contrôle (Inversion of Control, IoC). Au lieu que l'objet crée lui-même ses dépendances, celles-ci sont "injectées" de l'extérieur. En TypeScript, ce pattern est particulièrement utile grâce au typage strict et aux possibilités des décorateurs.
Imaginez que vous construisez une application pour une boutique en ligne. Vous avez une classe OrderService qui doit envoyer des notifications. Sans DI, elle serait fortement liée à un moyen d'envoi spécifique (par exemple, Email). Avec DI, vous pouvez facilement remplacer Email par SMS ou des notifications Push, sans modifier le code de OrderService.
Pourquoi utiliser DI en TypeScript ?
- Testabilité : Il est facile de remplacer les services réels par des objets mock dans les tests unitaires.
- Flexibilité : Le système devient modulaire, les dépendances peuvent être changées sans modifier le code client.
- Simplification de la maintenance : Chaque classe est responsable uniquement de sa logique métier, et non de la création d'autres objets.
- Sécurité de typage : TypeScript vérifie les types à la compilation, ce qui réduit le nombre d'erreurs lors de l'exécution.
Implémentation de base de DI en TypeScript sans frameworks
Commençons par un exemple simple. Créons une interface pour le service de notifications et deux de ses implémentations.
// Définissons l'interfaceinterface INotificationService { send(message: string): void;}
// Implémentation pour Emailclass EmailNotificationService implements INotificationService { send(message: string): void { console.log(`Envoi Email : ${message}`); }}
// Implémentation pour SMSclass SMSNotificationService implements INotificationService { send(message: string): void { console.log(`Envoi SMS : ${message}`); }}
// Classe qui utilise DIclass OrderService { constructor(private notificationService: INotificationService) {}
processOrder(orderId: number): void { // Logique de traitement de commande this.notificationService.send(`Commande #${orderId} traitée`); }}
// Utilisationconst emailService = new EmailNotificationService();const orderService = new OrderService(emailService);orderService.processOrder(123); // Envoi Email : Commande #123 traitéeDans cet exemple, OrderService ne sait pas quel service de notifications est utilisé. Il appelle simplement la méthode send via l'interface.
DI avancé : Création d'un conteneur IoC simple
Pour les grands projets, l'injection manuelle de dépendances devient lourde. C'est là qu'intervient le conteneur IoC. Voyons comment l'implémenter en utilisant les décorateurs TypeScript.
Étape 1 : Activez le support des décorateurs dans tsconfig.json
{ "compilerOptions": { "experimentalDecorators": true, "emitDecoratorMetadata": true }}Étape 2 : Créez le conteneur DI
// di-container.tstype Constructor<T> = new (...args: any[]) => T;
class DIContainer { private services = new Map<string, any>();
register<T>(token: string, implementation: Constructor<T>): void { this.services.set(token, implementation); }
resolve<T>(token: string): T { const Implementation = this.services.get(token); if (!Implementation) { throw new Error(`Le service "${token}" n'est pas enregistré`); }
// Obtenons les types des paramètres du constructeur via la réflexion const paramTypes: Constructor<any>[] = Reflect.getMetadata('design:paramtypes', Implementation) || [];
// Résolvons récursivement les dépendances const dependencies = param