Skip to main content

L’injection de dépendances en JavaScript

Écrit par

Jerry Ejonavi

feature buffer overflow

17 novembre 2022

0 minutes de lecture

Les techniques d’inversion de contrôle (IoC) permettent aux développeurs de sortir du flux de programmation traditionnel et leur offrent davantage de flexibilité et de contrôle sur leur code. L’injection de dépendances, une forme d’IoC, est un modèle qui vise à séparer la construction des objets de leur utilisation.

Dans cet article, vous découvrirez ce qu’est l’injection de dépendances, quand l’utiliser et dans quels frameworks JavaScript populaires elle est implémentée.

Qu’est-ce que l’injection de dépendances ?

L’injection de dépendances est un modèle de conception logicielle dans lequel un objet ou une fonction utilise d’autres objets ou fonctions (les dépendances) sans se préoccuper de leurs détails d’implémentation. La tâche consistant à fournir aux objets les dépendances dont ils ont besoin est confiée à un injecteur, parfois appelé assembleur, fournisseur ou conteneur.

Prenons l’exemple des consoles de jeux vidéo : elles ont simplement besoin d’un disque ou d’une cartouche compatible pour fonctionner. Les différents disques ou cartouches contiennent généralement les données de jeux différents. Le joueur n’a pas besoin de connaître le fonctionnement interne de la console et, en général, il lui suffit d’insérer ou de remplacer le disque du jeu pour jouer.

Concevoir un logiciel ou écrire du code de cette manière facilite le retrait ou le remplacement de composants (comme dans l’exemple ci-dessus), l’écriture de tests unitaires et la réduction du code répétitif dans une base de code.

Bien que l’injection de dépendances soit un modèle couramment implémenté et utilisé dans d’autres langages, notamment Java, C# et Python, on la rencontre moins souvent en JavaScript. Dans les sections suivantes, vous découvrirez les avantages et les inconvénients de ce modèle, son implémentation dans des frameworks JavaScript populaires et la façon de configurer un projet JavaScript en prévision de l’utilisation de l’injection de dépendances.

Pourquoi avez-vous besoin de l’injection de dépendances ?

Pour bien comprendre l’utilité de l’injection de dépendances, il est important de comprendre les problèmes auxquels répondent les techniques d’IoC. Reprenons l’analogie de la console de jeux vidéo évoquée plus tôt et essayons de représenter le fonctionnement d’une console en code. Dans cet exemple, nous appellerons votre console NoSleepStation et partirons des hypothèses suivantes :

  • Une console NoSleepStation ne peut exécuter que les jeux conçus pour la NoSleepStation.

  • La seule source d’entrée valide pour la console est un disque compact.

À partir de ces informations, on peut implémenter la console NoSleepStation de la manière suivante :

// The GameReader class
class GameReader {
  constructor(input) {
    this.input = input;
  }
  readDisc() {
    console.log("Now playing: ", this.input);
  }
}

// The NoSleepStation Console class
class NSSConsole {
  gameReader = new GameReader("TurboCars Racer");

  play() {
    this.gameReader.readDisc();
  }
}

// use the classes above to play 
const nssConsole = new NSSConsole();
nssConsole.play();

Ici, la logique principale de la console se trouve dans la classe GameReader, dont dépend NSSConsole. La méthode play de la console lance un jeu à l’aide d’une instance de GameReader . Plusieurs problèmes sont toutefois manifestes, notamment en matière de flexibilité et de tests.

Flexibilité

Le code présenté précédemment manque de flexibilité. Si un utilisateur voulait jouer à un autre jeu, il devrait modifier la classe NSSConsole, ce qui reviendrait à démonter la console dans la vraie vie. En effet, une dépendance essentielle, la classe GameReader, est codée en dur dans l’implémentation de NSSConsole.

L’injection de dépendances résout ce problème en dissociant les classes de leurs dépendances et en ne fournissant ces dernières qu’au moment où elles sont nécessaires. Dans l’exemple de code précédent, la classe NSSConsole a simplement besoin de la méthode readDisc() d’une instance de GameReader.

Avec l’injection de dépendances, le code précédent peut être réécrit ainsi :

class GameReader {
  constructor(input) {
    this.input = input;
  }

  readDisc() {
    console.log("Now playing: ", this.input);
  }

  changeDisc(input) {
    this.input = input;
    this.readDisc();
  }
}

class NSSConsole {
  constructor(gameReader) {
    this.gameReader = gameReader;
  }

  play() {
    this.gameReader.readDisc();
  }

  playAnotherTitle(input) {
    this.gameReader.changeDisc(input);
  }
}

const gameReader = new GameReader("TurboCars Racer");
const nssConsole = new NSSConsole(gameReader);

nssConsole.play();
nssConsole.playAnotherTitle("TurboCars Racer 2");

Le changement le plus important dans ce code est que les classes NSSConsole et GameReader sont désormais dissociées. Bien qu’une instance de NSSConsole ait toujours besoin d’une instance de GameReader pour fonctionner, elle n’a pas à la créer explicitement. La création de l’instance de GameReader et sa transmission à NSSConsole sont confiées au fournisseur d’injection de dépendances.

Dans une base de code volumineuse, cela peut réduire considérablement le code répétitif, puisque la création et le raccordement des dépendances sont pris en charge par un fournisseur d’injection de dépendances. Vous n’avez donc pas à vous soucier de créer les instances des classes dont une classe donnée a besoin, surtout si ces dépendances ont elles-mêmes leurs propres dépendances.

Tests

L’un des principaux avantages de l’injection de dépendances concerne les tests unitaires. En confiant à un fournisseur externe la tâche de fournir les dépendances d’une classe, on peut utiliser des mocks ou des stubs à la place des objets qui ne sont pas testés :

// nssconsole.test.js
const gameReaderStub = {
  readDisc: () => {
    console.log("Read disc");
  },
  changeDisc: (input) => {
    console.log("Changed disc to: " + input);
  },
};

const nssConsole = new NSSConsole(gameReaderStub);

Tant que l’interface de la dépendance (les méthodes et propriétés exposées) ne change pas, un objet simple doté de la même interface peut remplacer une instance réelle lors d’un test unitaire.

Limites de l’injection de dépendances

Il est important de noter que l’injection de dépendances présente aussi des inconvénients, généralement liés à sa façon de résoudre le problème de la réduction du code répétitif.

Pour tirer le meilleur parti de l’injection de dépendances, en particulier dans une base de code volumineuse, il faut configurer un fournisseur d’injection de dépendances. Cette configuration peut demander beaucoup de travail et représenter une charge inutile dans un projet simple.

Il est possible de résoudre ce problème à l’aide d’une bibliothèque ou d’un framework d’injection de dépendances. Toutefois, dans la plupart des cas, l’utilisation d’un framework d’injection de dépendances exige de s’y engager pleinement, car les frameworks de ce type offrent peu de similitudes d’API lors de la configuration.

L’injection de dépendances dans les frameworks JavaScript populaires

L’injection de dépendances est une fonctionnalité clé de nombreux frameworks JavaScript populaires, notamment Angular(), NestJS et Vue.js.

Angular

Angular est un framework de développement web côté client qui comprend les outils nécessaires pour créer, tester et maintenir des applications web. L’injection de dépendances est intégrée au framework : aucune configuration n’est nécessaire et il s’agit de la méthode recommandée pour développer avec Angular.

Voici un exemple de code montrant le fonctionnement de l’injection de dépendances dans Angular (extrait de la documentation Angular) :

// logger.service.ts
import { Injectable } from '@angular/core';

@Injectable({providedIn: 'root'})
export class Logger {
  writeCount(count: number) {
    console.warn(count);
  }
}

// hello-world-di.component.ts
import { Component } from '@angular/core';
import { Logger } from '../logger.service';

@Component({
  selector: 'hello-world-di',
  templateUrl: './hello-world-di.component.html'
})
export class HelloWorldDependencyInjectionComponent  {
  count = 0;

  constructor(private logger: Logger) { }

  onLogMe() {
    this.logger.writeCount(this.count);
    this.count++;
  }
}

Dans Angular, l’utilisation du décorateur @Injectable sur une classe indique que celle-ci peut être injectée. Une classe injectable peut être mise à la disposition de ses dépendants de trois façons :

  • Au niveau du composant, à l’aide du champ providers du décorateur @Component

  • Au niveau de NgModule, à l’aide du champ providers du décorateur @NgModule

  • À la racine de l’application, en ajoutant providedIn: 'root' à un décorateur @Injectable (comme vu précédemment)

Pour injecter une dépendance dans Angular, il suffit de la déclarer dans le constructeur d’une classe. Dans l’exemple de code précédent, la ligne suivante montre comment faire :

constructor(private logger: Logger) // Angular injects an instance of the LoggerService class

NestJS

NestJS est un framework JavaScript dont l’architecture permet de créer des applications backend très performantes, fiables et évolutives. Comme sa conception s’inspire largement d’Angular, l’injection de dépendances fonctionne de manière très similaire dans NestJS :

// logger.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class Logger {
  writeCount(count: number) {
    console.warn(count);
  }
}

// hello-world-di.component.ts
import { Controller } from '@nestjs/common';
import { Logger } from '../logger.service';

@Controller('')
export class HelloWorldDependencyInjectionController  {
  count = 0;

  constructor(private logger: Logger) { }

  onLogMe() {
    this.logger.writeCount(this.count);
    this.count++;
  }
}

Notez que dans NestJS et Angular, les dépendances sont généralement considérées comme des singletons. Cela signifie qu’une fois trouvée, la valeur d’une dépendance est mise en cache et réutilisée pendant tout le cycle de vie de l’application. Pour modifier ce comportement dans NestJS, vous devez configurer la propriété `scope` des options du décorateur @Injectable. Dans Angular, vous devez configurer la propriété `providedIn` des options du décorateur @Injectable.

Vue.js

Vue.js est un framework JavaScript déclaratif et basé sur des composants, conçu pour créer des interfaces utilisateur. Dans Vue.js, l’injection de dépendances peut être configurée à l’aide des options `provide` et `inject` lors de la création d’un composant.

Pour indiquer quelles données doivent être mises à la disposition des descendants d’un composant, utilisez l’option provide :

export default {
provide: {
message: 'hello!',
}
}

Vous pouvez ensuite injecter ces dépendances dans les composants qui en ont besoin à l’aide de l’option inject :

export default {
  inject: ['message'],
  created() {
    console.log(this.message) // injected value
  }
}

Pour fournir une dépendance au niveau de l’application, comme avec providedIn: 'root' dans Angular, utilisez la méthode provide de l’instance de l’application Vue.js :

import { createApp } from 'vue'

const app = createApp({})

app.provide(/* key */ 'message', /* value */ 'hello!')

Frameworks d’injection de dépendances JavaScript

Jusqu’ici, vous avez découvert l’injection de dépendances dans le contexte d’un framework JavaScript. Mais si quelqu’un souhaite démarrer un projet sans utiliser aucun des frameworks mentionnés et sans implémenter de conteneur ou de résolveur d’injection de dépendances à partir de zéro ?

Quelques bibliothèques proposent des fonctionnalités d’injection de dépendances pour ce cas de figure, notamment injection-js, InversifyJS et TSyringe. La section suivante se concentre sur InversifyJS, mais il peut être utile d’examiner les autres packages pour voir s’ils répondent mieux à vos besoins.

InversifyJS

InversifyJS est un conteneur d’injection de dépendances pour JavaScript. Il est conçu pour réduire au minimum la surcharge à l’exécution, tout en facilitant et en encourageant les bonnes pratiques de programmation orientée objet (POO) et d’IoC. Le dépôt du projet se trouve sur GitHub.

Pour utiliser InversifyJS dans un projet, il faut l’ajouter aux dépendances du projet. Nous allons configurer InversifyJS avec l’exemple de jeu utilisé tout au long de cet article. Créez donc un nouveau projet avec npm init. Ouvrez un terminal et exécutez les commandes suivantes dans l’ordre :

mkdir inversify-di
cd inversify-di
npm init

Ajoutez ensuite inversify aux dépendances du projet à l’aide de npm ou de Yarn :

# Using npm
npm install inversify reflect-metadata

# or if you use yarn: 
# yarn add inversify reflect-metadata

InversifyJS s’appuie sur l’API de réflexion des métadonnées ; vous devez donc installer et utiliser le package reflect-metadata comme polyfill.

Ajoutez ensuite le code des classes NSSConsole et GameReader en créant d’abord deux fichiers vides :

touch game-reader.mjs nssconsole.mjs

Ajoutez ensuite le code suivant à chacun des fichiers :

// game-reader.mjs
export default class GameReader {
  constructor(input = "TurboCars Racer") {
    this.input = input;
  }

  readDisc() {
    console.log("Now playing: ", this.input);
  }

  changeDisc(input) {
    this.input = input;
    this.readDisc();
  }
}
// nssconsole.mjs
export default class NSSConsole {
  constructor(gameReader) {
    this.gameReader = gameReader;
  }

  play() {
    this.gameReader.readDisc();
  }

  playAnotherTitle(input) {
    this.gameReader.changeDisc(input);
  }
}

Enfin, configurez le projet pour utiliser InversifyJS :

touch config.mjs index.mjs
// config.mjs
import { decorate, injectable, inject, Container } from "inversify";
import GameReader from "./game-reader.mjs";
import NSSConsole from "./nssconsole.mjs";

// Declare our dependencies' type identifiers
export const TYPES = {
  GameReader: "GameReader",
  NSSConsole: "NSSConsole",
};

// Declare injectables
decorate(injectable(), GameReader);
decorate(injectable(), NSSConsole);

// Declare the GameReader class as the first dependency of NSSConsole
decorate(inject(TYPES.GameReader), NSSConsole, 0);

// Declare bindings
const container = new Container();
container.bind(TYPES.NSSConsole).to(NSSConsole);
container.bind(TYPES.GameReader).to(GameReader);

export { container };
// index.mjs
// Import reflect-metadata as a polyfill
import "reflect-metadata";
import { container, TYPES } from "./config.mjs";

// Resolve dependencies
// Notice how we do not need to explicitly declare a GameReader instance anymore
const myConsole = container.get(TYPES.NSSConsole);

myConsole.play();
myConsole.playAnotherTitle("Some other game");

Vous pouvez tester la nouvelle configuration en exécutant node index.mjs à la racine du projet.

Bien que minimal, cet exemple peut constituer un bon point de départ pour la plupart des projets qui souhaitent utiliser InversifyJS.

Conclusion

Dans cet article, vous avez découvert l’injection de dépendances en JavaScript, ses avantages et ses inconvénients, des exemples dans des frameworks JavaScript populaires et la manière de l’utiliser dans un projet JavaScript vanilla.

Lire la suite

blog feature snyk container custom base recommendations
Blog

Découvrez Evo Agentic AppSec : une sécurité au rythme du développement de l’IA

Le développement piloté par l’IA dépasse les workflows de sécurité traditionnels. L’AppSec agentique de Snyk s’appuie sur des agents de sécurité pour détecter, corriger et vérifier les vulnérabilités en continu, tandis que les équipes définissent les exigences.

blog feature ai violet
Blog

VulnBump : les agents IA peuvent-ils corriger les dépendances vulnérables sans casser le build ?

VulnBump évalue des agents IA sur 307 mises à niveau de dépendances réelles. Une correction n’est comptabilisée que si la vulnérabilité disparaît et que la compilation et les tests restent au vert.

Blog

Les modèles de pointe ont trouvé les vulnérabilités. Seul l’attaquant a trouvé les chaînes d’exploitation.

L’analyse statique a détecté les failles, mais seuls des tests d’attaque en conditions réelles ont prouvé comment elles pouvaient être enchaînées pour provoquer des compromissions. Comparaison d’Evo COS, de Claude Security et de Claude Code Security.