Injection de Dépendances en TypeScript : guide pratique avec exemples

Formateur Python en ligne pour débutants

Apprenez Python facilement sans surcharge théorique. Résolvez des tâches pratiques avec vérification automatique et écrivez du code directement dans le navigateur.

Start Course

Injection de Dépendances en TypeScript : de la théorie à la pratique

\n\n

L'Injection de Dépendances (DI) est l'un des patrons de conception clés qui permet de créer des applications flexibles, testables et maintenables. En TypeScript, grâce au typage strict et aux possibilités des décorateurs, l'implémentation de la DI devient particulièrement élégante. Dans cet article, nous allons voir comment implémenter l'Injection de Dépendances en pratique, en utilisant TypeScript, et examiner pourquoi ce patron est une pierre angulaire du développement moderne.

\n\n

Qu'est-ce que l'Injection de Dépendances et pourquoi en a-t-on besoin ?

\n\n

L'Injection de Dépendances (DI) est un patron dans lequel un objet reçoit ses dépendances de l'extérieur, plutôt que de les créer lui-même. Au lieu d'écrire new SomeService() à l'intérieur d'une classe, nous passons une instance prête via le constructeur ou une méthode.

\n\n

Les principaux avantages de la DI :

\n
    \n
  • Réduction du couplage (Coupling) — les classes ne dépendent pas d'implémentations concrètes, mais uniquement d'abstractions (interfaces).
  • \n
  • Simplification des tests — nous pouvons facilement remplacer les services réels par des mocks ou des stubs.
  • \n
  • Augmentation de la réutilisabilité — les dépendances peuvent être remplacées sans modifier le code du consommateur.
  • \n
  • Respect du principe d'inversion des dépendances (DIP de SOLID) — les modules de haut niveau ne dépendent pas des modules de bas niveau.
  • \n
\n\n

Implémentation de l'Injection de Dépendances en TypeScript sans frameworks

\n\n

Commençons par un exemple simple. Supposons que nous ayons un service de journalisation et une classe qui l'utilise.

\n\n
// Sans DI - couplage fort\nclass Logger {\n    log(message: string): void {\n        console.log(`[LOG]: ${message}`);\n    }\n}\n\nclass UserService {\n    private logger: Logger;\n\n    constructor() {\n        this.logger = new Logger(); // Problème : dépendance forte\n    }\n\n    createUser(name: string): void {\n        this.logger.log(`Utilisateur créé : ${name}`);\n        // ... logique de création\n    }\n}\n\n// Avec DI - via le constructeur\ninterface ILogger {\n    log(message: string): void;\n}\n\nclass ConsoleLogger implements ILogger {\n    log(message: string): void {\n        console.log(`[CONSOLE LOG]: ${message}`);\n    }\n}\n\nclass FileLogger implements ILogger {\n    log(message: string): void {\n        // Logique d'écriture dans un fichier\n        console.log(`[FILE LOG]: ${message}`);\n    }\n}\n\nclass UserServiceWithDI {\n    constructor(private logger: ILogger) {} // Dépendance injectée de l'extérieur\n\n    createUser(name: string): void {\n        this.logger.log(`Utilisateur créé : ${name}`);\n    }\n}\n\n// Utilisation\nconst consoleLogger = new ConsoleLogger();\nconst userService = new UserServiceWithDI(consoleLogger);\nuserService.createUser('Alice'); // [CONSOLE LOG]: Utilisateur créé : Alice\n
\n\n

Comme vous pouvez le voir, maintenant UserServiceWithDI ne sait pas quel logger est utilisé. Nous pouvons facilement le remplacer par FileLogger ou un mock pour les tests.

\n\n

DI avancé : création d'un conteneur IoC simple

\n\n

Lorsque les dépendances deviennent nombreuses, l'injection manuelle via le constructeur devient lourde. C'est là qu'intervient le conteneur IoC (Inversion of Control Container). Il gère la création et le cycle de vie des objets.

\n\n

Implémentons un conteneur simple en TypeScript :

\n\n
// Type pour la fabrique de dépendances\ntype Factory<T> = (container: Container) => T;\n\nclass Container {\n    private factories = new Map<string, Factory<any>>();\n    private instances = new Map<string, any>();\n\n    // Enregistrement d'une dépendance (la fabrique est appelée à chaque demande)\n    register<T>(name: string, factory: Factory<T>): void {\n        this.factories.set(name, factory);\n    }\n\n    // Enregistrement d'un singleton (la fabrique est appelée une seule fois)\n    singleton<T>(name: string, factory: Factory<T>): void {\n        this.factories.set(name, factory);\n    }\n\n    // Résolution d'une dépendance\n    resolve<T>(name: string): T {\n        const factory = this.factories.get(name);\n        if (!factory) {\n            throw new Error(`Dépendance non trouvée : ${name}`);\n        }\n        // Logique de singleton simplifiée\n        if (!this.instances.has(name)) {\n            this.instances.set(name, factory(this));\n        }\n        return this.instances.get(name);\n    }\n}\n
\n\n

Ce conteneur permet d'enregistrer et de résoudre des dépendances de manière centralisée, simplifiant ainsi la gestion des dépendances dans des applications plus complexes.

Blogs

Book Recommendations