Avez-vous une question? (216) 97 656 803 hajjriadh@gmail.com
NestJS

Principes de l'injection de dépendances (DI) et de l'inversion de contrôle

Cette leçon explique les principes sur lesquels repose tout NestJS : la notion de dépendance, le problème du couplage fort quand une classe crée elle-même ses dépendances, l'injection de dépendances p...

0 chapitres 0 QCM Accès gratuit
Principes de l'injection de dépendances (DI) et de l'inversion de contrôle

À propos de ce cours

Cette leçon explique les principes sur lesquels repose tout NestJS : la notion de dépendance, le problème du couplage fort quand une classe crée elle-même ses dépendances, l'injection de dépendances par le constructeur, la programmation vers un contrat et l'inversion de contrôle confiée à un conteneur. Trois versions exécutées d'un même service d'inscription (couplage fort, injection manuelle, mini-conteneur) montrent concrètement le gain pour la maintenance et les tests, avant de faire le lien avec le conteneur de NestJS.

Objectifs de la leçon

  • Définir une dépendance et le couplage entre classes.
  • Expliquer l'injection de dépendances et son intérêt pour les tests.
  • Expliquer l'inversion de contrôle et le rôle d'un conteneur.
  • Faire le lien avec le fonctionnement de NestJS.
Qu'est-ce qu'une dépendance ?

Une classe dépend d'une autre lorsqu'elle a besoin d'elle pour fonctionner. Un service d'inscription a besoin d'un service d'envoi d'e-mails pour confirmer l'inscription ; un service de notes a besoin du service des stagiaires pour vérifier qu'un stagiaire existe. La vraie question est : qui crée la dépendance ?

Couplage fort, injection manuelle et conteneur

Trois réponses à la question « qui crée la dépendance ? ».

1. Le couplage fort : la classe crée sa dépendance
class EmailService {
  envoyer(a: string, message: string) {
    console.log(`[SMTP réel] e-mail à ${a} : ${message}`);
  }
}

class InscriptionService {
  private email = new EmailService();        // ✗ crée lui-même sa dépendance

  inscrire(nom: string, adresse: string) {
    this.email.envoyer(adresse, `Bienvenue ${nom}`);
    return { nom, inscrit: true };
  }
}

new InscriptionService().inscrire('Ben Ali', 'sami@exemple.tn');

Résultat réellement obtenu :

[SMTP réel] e-mail à sami@exemple.tn : Bienvenue Ben Ali

Ce code fonctionne, mais InscriptionService est soudé à EmailService:

  • Pour envoyer un SMS à la place, il faut modifier InscriptionService.
  • Pour le tester, impossible d'éviter l'envoi d'un vrai e-mail.
  • Si EmailService a lui-même besoin d'une configuration (serveur SMTP, mot de passe), InscriptionService doit la connaître pour faire le new.
2. L'injection de dépendances : la classe reçoit sa dépendance

On inverse la logique : la classe ne crée plus sa dépendance, elle la reçoit dans son constructeur. Mieux : elle ne dépend plus d'une classe précise mais d'un contrat (une interface), que plusieurs classes peuvent remplir.

interface Notificateur {
  envoyer(a: string, message: string): void;
}

class EmailService implements Notificateur {
  envoyer(a: string, message: string) {
    console.log(`[SMTP réel] e-mail à ${a} : ${message}`);
  }
}

class SmsService implements Notificateur {
  envoyer(a: string, message: string) {
    console.log(`[SMS] au ${a} : ${message}`);
  }
}

class NotificateurDeTest implements Notificateur {
  envois: string[] = [];
  envoyer(a: string, message: string) {
    this.envois.push(`${a} | ${message}`);     // n'envoie rien : enregistre
  }
}

class InscriptionService {
  constructor(private readonly notificateur: Notificateur) {}   // ✓ reçoit sa dépendance

  inscrire(nom: string, adresse: string) {
    this.notificateur.envoyer(adresse, `Bienvenue ${nom}`);
    return { nom, inscrit: true };
  }
}

// Le code « extérieur » choisit l'implémentation
new InscriptionService(new EmailService()).inscrire('Ben Ali', 'sami@exemple.tn');
new InscriptionService(new SmsService()).inscrire('Trabelsi', '+216 20 000 000');

const espion = new NotificateurDeTest();
new InscriptionService(espion).inscrire('Gharbi', 'yassine@exemple.tn');
console.log('Test :', espion.envois);

Résultat réellement obtenu :

[SMTP réel] e-mail à sami@exemple.tn : Bienvenue Ben Ali
[SMS] au +216 20 000 000 : Bienvenue Trabelsi
Test : [ 'yassine@exemple.tn | Bienvenue Gharbi' ]

InscriptionService n'a pas changé d'une ligne entre les trois utilisations. Le dernier cas montre l'intérêt majeur pour les tests: on lui fournit une fausse implémentation qui enregistre les envois au lieu de les faire, et on vérifie ce qui aurait été envoyé.

Définition: l'injection de dépendances (Dependency Injection, DI) consiste à fournir à un objet les objets dont il a besoin, au lieu qu'il les crée lui-même. La forme la plus courante, celle de NestJS, est l'injection par le constructeur.

3. L'inversion de contrôle : un conteneur fait les « new »

Avec l'injection manuelle, il faut encore écrire quelque part tous les new, dans le bon ordre. Dans une application de 50 services, c'est ingérable. On confie alors ce travail à un conteneur: on lui décrit comment fabriquer chaque élément, et il s'occupe de les créer, de les relier et de les réutiliser.

// Un conteneur minimal : il sait QUI fabriquer pour chaque besoin, et garde une instance de chacun
type Fabrique<T> = (c: Conteneur) => T;

class Conteneur {
  private fabriques = new Map<string, Fabrique<unknown>>();
  private instances = new Map<string, unknown>();

  enregistrer<T>(nom: string, fabrique: Fabrique<T>) {
    this.fabriques.set(nom, fabrique);
  }

  obtenir<T>(nom: string): T {
    if (!this.instances.has(nom)) {
      const fabrique = this.fabriques.get(nom);
      if (!fabrique) throw new Error(`Aucun fournisseur pour « ${nom} »`);
      console.log(`  (création de ${nom})`);
      this.instances.set(nom, fabrique(this));
    }
    return this.instances.get(nom) as T;
  }
}

class EmailService {
  envoyer(a: string, m: string) { console.log(`[SMTP] ${a} : ${m}`); }
}
class InscriptionService {
  constructor(private readonly email: EmailService) {}
  inscrire(nom: string, adresse: string) { this.email.envoyer(adresse, `Bienvenue ${nom}`); }
}

const conteneur = new Conteneur();
conteneur.enregistrer('EmailService', () => new EmailService());
conteneur.enregistrer('InscriptionService', (c) => new InscriptionService(c.obtenir('EmailService')));

console.log('Première demande :');
conteneur.obtenir<InscriptionService>('InscriptionService').inscrire('Mansouri', 'lina@exemple.tn');
console.log('Deuxième demande :');
conteneur.obtenir<InscriptionService>('InscriptionService').inscrire('Jlassi', 'omar@exemple.tn');

Résultat réellement obtenu :

Première demande :
  (création de InscriptionService)
  (création de EmailService)
[SMTP] lina@exemple.tn : Bienvenue Mansouri
Deuxième demande :
[SMTP] omar@exemple.tn : Bienvenue Jlassi

Observez : à la première demande, le conteneur crée InscriptionService, s'aperçoit qu'il lui faut un EmailService et le crée aussi ; à la seconde, il réutilise les mêmes instances. Ce n'est plus votre code qui contrôle la création des objets, c'est le conteneur : on parle d'inversion de contrôle (Inversion of Control, IoC).

4. Et NestJS ?

NestJS possède un conteneur de ce type, bien plus complet. Vous n'avez rien à « enregistrer » à la main :

Mini-conteneur ci-dessusNestJS
conteneur.enregistrer('EmailService', …)providers: [EmailService] dans un module
la fabrique fait new InscriptionService(c.obtenir('EmailService'))NestJS lit le type des paramètres du constructeur (métadonnées, leçon 4.7)
conteneur.obtenir('InscriptionService')injection automatique dans le constructeur du contrôleur
une instance gardée dans une Mapportée singleton par défaut (leçon 7.5)
un nom (chaîne) par élémentun jeton: la classe elle-même, ou une chaîne, ou un Symbol (leçon 7.4)
// L'équivalent NestJS des trois versions ci-dessus
@Injectable()
export class EmailService {
  envoyer(a: string, message: string) { /* … */ }
}

@Injectable()
export class InscriptionService {
  constructor(private readonly email: EmailService) {}   // reçu, jamais créé
}

@Module({ providers: [EmailService, InscriptionService] })
export class InscriptionsModule {}
Les bénéfices, en résumé
  • Faible couplage: chaque classe ne connaît que ce dont elle a besoin, pas la façon de le fabriquer.
  • Testabilité: on remplace une dépendance par une version de test (leçon 7.4 et chapitre 17).
  • Flexibilité: on change d'implémentation (e-mail ou SMS, MySQL ou mémoire) par configuration.
  • Partage: une seule instance d'un service coûteux (connexion, cache) pour toute l'application.
Activités

Activité 1 Repérer le couplage

Dans le code ci-dessous, identifiez toutes les dépendances créées « en dur » et expliquez pourquoi ce service est difficile à tester.

class BulletinService {
  private db = new ConnexionMySQL('localhost', 'root', 'secret');
  private pdf = new GenerateurPdf();
  private mail = new EmailService();

  envoyerBulletin(id: number) {
    const notes = this.db.requete(`SELECT * FROM notes WHERE stagiaire = ${id}`);
    const fichier = this.pdf.creer(notes);
    this.mail.envoyer('parent@exemple.tn', fichier);
  }
}
Voir le corrigé

Trois dépendances en dur : connexion MySQL (avec des identifiants écrits dans le code !), générateur PDF et service d'e-mail. Tester envoyerBulletin exigerait une vraie base, créerait un vrai PDF et enverrait un vrai e-mail. En recevant les trois dans le constructeur, on peut fournir en test une fausse base qui renvoie des notes fixes, un faux générateur et un faux envoi. Remarquez aussi la requête SQL construite par concaténation, vulnérable à l'injection SQL (chapitre 11).

Activité 2 Refactoriser

Réécrivez BulletinService pour qu'il reçoive ses trois dépendances. Définissez pour chacune une interface minimale (SourceNotes, Generateur, Expediteur), puis écrivez trois fausses implémentations et un test qui vérifie que le bulletin du stagiaire 7 est envoyé.

Voir le corrigé
interface SourceNotes { notesDe(id: number): number[] }
interface Generateur { creer(notes: number[]): string }
interface Expediteur { envoyer(a: string, piece: string): void }

class BulletinService {
  constructor(
    private readonly source: SourceNotes,
    private readonly generateur: Generateur,
    private readonly expediteur: Expediteur,
  ) {}
  envoyerBulletin(id: number, destinataire: string) {
    const fichier = this.generateur.creer(this.source.notesDe(id));
    this.expediteur.envoyer(destinataire, fichier);
  }
}

// Test
const envoyes: string[] = [];
const service = new BulletinService(
  { notesDe: () => [12, 15] },
  { creer: (n) => `bulletin(${n.join(',')})` },
  { envoyer: (a, p) => envoyes.push(`${a}:${p}`) },
);
service.envoyerBulletin(7, 'parent@exemple.tn');
console.log(envoyes);   // [ 'parent@exemple.tn:bulletin(12,15)' ]

Activité 3 Enrichir le mini-conteneur

Ajoutez au mini-conteneur de la partie 3 une méthode remplacer(nom, fabrique) qui remplace une fabrique et efface l'instance existante. Utilisez-la pour remplacer EmailService par une version de test avant la première demande. À quelle fonctionnalité de NestJS cela correspond-il ?

Voir le corrigé
remplacer<T>(nom: string, fabrique: Fabrique<T>) {
  this.fabriques.set(nom, fabrique);
  this.instances.delete(nom);
}

// avant d'obtenir InscriptionService :
conteneur.remplacer('EmailService', () => ({ envoyer: (a: string) => console.log(`[faux] ${a}`) }));

C'est l'équivalent de Test.createTestingModule(...).overrideProvider(EmailService).useValue(...) de @nestjs/testing, présenté à la leçon 7.4 et au chapitre 17. Attention : si InscriptionService avait déjà été créé, il garderait l'ancien EmailService. Il faudrait aussi effacer les instances qui en dépendent.

Activité 4 Vrai ou faux ?

(a) L'injection de dépendances impose d'utiliser un framework. (b) Avec l'injection, une classe ne contient plus jamais le mot new. (c) L'inversion de contrôle signifie que le conteneur décide quand créer les objets. (d) Dans NestJS, un service est recréé à chaque injection. (e) L'injection facilite les tests unitaires.

Voir le corrigé

(a) Faux : la partie 2 la pratique en TypeScript pur. (b) Faux : on crée toujours des objets de données (new Date(), new Map()) ; ce sont les services dont on dépend qu'on ne crée plus. (c) Vrai. (d) Faux : par défaut, une seule instance est partagée (leçon 7.5). (e) Vrai.

À retenir

  • Une dépendance est un objet dont une classe a besoin pour fonctionner.
  • Couplage fort : la classe fait elle-même new → difficile à changer et à tester.
  • Injection de dépendances : la classe reçoit ses dépendances, de préférence par le constructeur.
  • Inversion de contrôle : un conteneur crée, relie et réutilise les objets.
  • NestJS est ce conteneur : providers dans les modules, injection lue dans les constructeurs.