Node.js : l’inversion de contrôle (IOC) et l’injection de dépendances (DI)

Injection de dépendances

Aujourd’hui, je vais tâcher d’expliquer le principe de l’inversion de contrôle (IOC) et de l’injection de dépendances (DI) à travers un exemple en Node.js mais le principe est valable pour d’autres langages. Je vous présenterai à la fin de cet article une librairie de conteneur IOC que j’ai développé pour mes besoins personnels.

“IOC”, “DI” mais c’est quoi au juste?

L’inversion de contrôle ou IOC pour inversion of control est un principe général de conception dont le but est de diminuer le couplage entre les composants logiciels.

L’injection de dépendances ou DI pour “dependency injection”, est-elle une méthode permettant d’appliquer le principe d’IOC. Le principe est tout simple, il consiste à passer (injecter) un composant logiciel (une classe par exemple) à un autre composant logiciel qui l’utilise, permettant ainsi d’éviter la dépendance d’un composant à un autre et ainsi améliorer la souplesse de l’application.

Prenons un exemple pour illustrer tout ça. Imaginons que l’on est une classe qui permet de logger les erreurs dans un fichier :

Et qu’on utilise ensuite notre logger dans une autre classe, par exemple une classe qui permet d’envoyer des emails (j’omets volontairement le reste du code ce qui nous intéresse ici c’est le constructeur) :

Ce code fonctionne, mais il y a un souci, si l’on souhaite changer le nom du fichier de log on est bloqué. On peut très bien passer les paramètres du logger dans le constructeur de la classe Mailer:

Cette solution fonctionne, mais plusieurs problèmes se posent :

  • Les paramètres logPath et logFilename ne concernent pas directement la classe Mailer mais la classe FileLogger. On a donc un souci de logique et de responsabilité, ce n’est pas à la classe Mailer de créer une instance de FileLogger,
  • La classe Mailer ne peut pas utiliser un autre type de logger (un logger qui afficherait les logs dans la console par exemple), ce qui pose un vrai souci d’évolution,
  • Si dans le futur la classe FileLogger venait à changer cela pourrait avoir un impact sur la classe Mailer. Là ce n’est pas très grave, mais imaginé que la classe FileLogger soit utilisée dans plusieurs centaines de fichiers, cela deviendrait vite ingérable.

La solution est donc d’injecter l’objet logger directement dans le constructeur de la classe Mailer :

Cette solution offre beaucoup plus de flexibilité, les deux classes sont maintenant indépendantes l’une de l’autre.

On peut également “injecter” le logger via un “setter” permettant ainsi de changer le type de logger dynamiquement :

Afin de s’assurer que la classe Mailer reçoit bien un logger possédant une méthode log, une interface aurait été utilisée dans la plupart des autres langages orientés objet. Une interface permet de définir un contrat, dans notre cas chaque classe implémentant l’interface devra avoir une méthode log. La classe Mailer attendra donc en paramètre du constructeur et du setter une classe implémentant cette interface garantissant ainsi l’existence de la méthode log.

Malheureusement, Javascript n’a pas la notion d’interface (pour information Javascript n’est pas vraiment un langage orienté objet mais un langage orienté prototype), trois choix s’offre alors à nous:

  • On est rigoureux lorsque l’on utilisera votre classe Mailer en passant réellement en paramètre un logger avec une méthode log,
  • On imite le comportement d’une interface (mais j’en parlerais dans le prochain article #teaser),
  • On utilise la méthode du duck typing.

Le duck typing est simple à comprendre, il fait référence au test du canard qui dit : “Si ça ressemble à un canard, si ça nage comme un canard et si ça cancane comme un canard, c’est qu’il s’agit sans doute d’un canard”. Dans notre cas, si le paramètre logger de notre classe Mailer possède une fonction log c’est qu’il s’agit sans doute d’un logger. On peut donc vérifier dans notre constructeur (et notre setter) que le paramètre logger possède une fonction log :

Conteneur IOC

Dans l’exemple précédent, notre logger doit être créé avant d’être injecté dans notre classe Mailer :

Imaginons maintenant que l’instance de FileLogger soit partagée entre plusieurs classes et que notre classe Mailer soit un service utilisé également dans d’autres classes, cela peut rapidement devenir compliqué pour gérer l’ensemble des dépendances, c’est là qu’intervient le conteneur IOC.

Le conteneur IOC va être chargé de créer et stocker les objets ainsi que résoudre les dépendances entre les objets tout en s’appuyant sur un fichier de configuration (bien que non obligatoire).

J’ai développé il y a quelque temps un module de conteneur IOC pour Node.js pour mes besoins personnels, que j’ai récemment mis à disposition sur npm : https://www.npmjs.com/package/smart-container

Voyons notre exemple précédent en utilisant ce module. On commence tout d’abord par notre fichier de configuration qui permettra de définir comment nos classes doivent être instanciées ainsi que définir leurs dépendances :

L’objet properties contient les paramètres de nos classes (ici les paramètres des classes Mailer et FileLogger) et l’objet services contient la définition de nos classes et de leurs dépendances.  Je vous invite à lire la documentation afin de comprendre son fonctionnement.

Il suffit ensuite simplement de construire notre conteneur comme suit :

Le conteneur va se charger d’instancier la classe FileLogger et de l’injecter dans la classe Mailer. On peut maintenant facilement accéder aux instances de nos classes Mailer et logger comme ceci :

L’avantage du conteneur IOC est que vous n’avez plus à vous préoccuper de la création de vos classes, vous devez simplement définir comment vos objets doivent être construits et le conteneur s’occupe de tout.

Il existe également d’autres modules de conteneurs IOC comme :

C’était simple non ?

Comme vous avez pu le voir, le principe de l’injection de dépendances est tout simple, certains d’entre vous appliquent déjà ce concept sans même le savoir. J’ai tout même voulu faire un article dessus, car je vois encore beaucoup de code avec des instanciations (new) directement dans le constructeur ce qui rend le code difficilement maintenable dès qu’une classe est modifiée. J’espère donc que cet article vous aura été utile, le prochain article portera sur les interfaces en Javascript ou plutôt comment imité une interface en Javascript.


Annonces partenaire

Je suis lead developer dans une boîte spécialisée dans l'univers du streaming/gaming, et en parallèle, je m'éclate en tant que freelance. Passionné par l'écosystème JavaScript, je suis un inconditionnel de Node.js depuis 2011. J'adore échanger sur les nouvelles tendances et partager mon expérience avec les autres développeurs. Si vous avez envie de papoter, n'hésitez pas à me retrouver sur Twitter, m'envoyer un petit email ou même laisser un commentaire.

4 commentaires

    1. Merci, je ne connaissais pas simple-dijs. J’ai développé cette librairie (smart-container) car je ne trouvais pas de librairie qui répondait à mes besoins notamment la création et l’injection de services via un fichier de configuration. Il est bien sûr possible de le faire avec les autres librairies, mais cela demande à chaque fois du développement en plus.

  1. Article simple et pédagogique, merci !

    Tu as mis en avant l’intérêt de l’inversion de contrôle pour réduire le couplage entre les module.

    Un autre avantage non négligeable de l’injection de dépendances est que cela facilite grandement les tests unitaires des modules, en évitant de laisser les modules charger leurs dépendances eux-mêmes.
    Cela manque d’ailleurs étonnamment d’exemples sur le web.

    Un autre point qui pourrait être évoqué pourrait être le parallèle avec l’inversion de contrôle dans les back-end Java, qui est un standard depuis des années (grâce à Spring).

    1. Merci pour ton commentaire.

      Évidemment cela permet de faciliter les tests unitaires et de “mocker” certaines parties de notre application, tu fais bien de le remarquer 😉

      L’inversion de contrôle est également fortement utilisée dans le framework PHP Symfony.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.