Dependency Injection : reprenez le contrôle de votre code.

Cet article fait partie d’une série sur les patrons de conception. Vous pouvez retrouver tous les autres ici (avec une introduction tout en bas).


Injection de dépendances (Dependancy Injection) : patron de conception consistant à construire les dépendances d’un module hors de celui-ci, afin d’obtenir un maximum de contrôle.


Problème

En novembre 2016, Nintendo annonce une nouvelle console qui fit frétiller les fans de la société : la NES Mini.

Conçue pour émuler l’expérience de jeux à l’ancienne, elle a le design (en plus petit), les manettes, et bien sûr, les jeux qui ont fait notre enfance.

Mais cette fois-ci, plus le cartouche, tous les jeux sont directement installés dans la console. On a juste à allumer le bousin, sélectionner un jeu, et l’on est parti pour quelques minutes de fun avant de la ranger dans un placard et de ne plus jamais y toucher.

En programmation, il s’agirait d’un module à contrôles directs, c’est-à-dire qui contrôle ses propres dépendances. On a juste à choisir un jeu dans le menu et la console se charge du reste.

C’est le genre de programme qui donne lieu à un bon gros switch des familles.
Sympa, mais comment je fais si je veux jouer à Deadly Towers ? Il n’est pas dans la console de base.

Et bien, vous ne pouvez pas, car la NES Mini a été construite pour faire tourner un catalogue de 30 jeux, et pas un de plus.

Donc si vous voulez prendre un pic de nostalgie en jouant Deadly Towers, Super Pitfall, Muppet Adventure ou n’importe quelle m#### du genre, vous n’avez que vos yeux pour pleurer.

Il y a bien des tutos qui vous donneront des solutions, mais elles sont toutes plus pourries les unes que les autres :

  • Vous pouvez trafiquer votre console pour ajouter les jeux que vous voulez. Ça marche jusqu’à ce que vous fassiez une mauvaise manip, et hop, poubelle.
  • Vous pouvez créer votre propre NES Mini avec une Raspberry, un peu de plastoc et du temps à perdre. Cool, maintenant, vous avez une deuxième console quasiment identique à la première.

Que vous pétiez ou remplaciez votre NES Mini, dans tous les cas, vos 100€ ne reviendront jamais.

Image originale de freepik.com

C’est le gros problème de ce genre de module. Ils contrôlent tout eux-mêmes, et vous n’avez plus votre mot à dire.

Tout fonctionne parfaitement, jusqu’au moment où vous voulez ajouter ou modifier quelque chose, et là, ça part en vrille. Vous êtes obligé soit de :

Malheureusement, il n’y a pas de bons choix, les deux issues sont terribles et engrangent une quantité de dette technique épouvantable.


Solution

Pour résoudre ce problème, l’astuce est de construire les dépendances d’un module à sa place. Prenons l’exemple de la NES de 1985.

Contrairement à NES Mini, vous ne sélectionnez pas un jeu à l’intérieur de la console, vous insérez une cartouche.

Si elle est capable de la lire, elle le fera, peu importe le jeu. Qu’il s’agisse de Super Mario Bros, son adaptation nord-coréenne ou votre propre adaptation faite maison, elle s’adaptera à tout ce que vous lui donnez.

C’est une forme d’inversion des contrôles appelée injection de dépendance.

Ce principe dit qu’un service (jeux) ne devrait jamais être construit à l’intérieur d’un client (console), mais injecté de l’extérieur.

Si votre client est une fonction, alors au lieu d’initialiser un service (objet) à l’intérieur de celui-ci, vous lui passez en paramètre.

C’est un des patrons de conception les plus stupidement simples, et pourtant, il va complètement révolutionner votre manière de programmer, car il permet :

  • De vous donner un contrôle TOTAL sur les données et le comportement du service.
  • D’appliquer et de profiter efficacement du principe d’inversion des dépendances (DIP).
  • De ne plus avoir à construire le service à l’intérieur du client, ce qui permet une meilleure séparation des responsabilités (SRP).
  • De vous débarrasser des longues listes de paramètres à n’en plus finir, car vous n’avez plus qu’un paramètre par service.
  • De faciliter les tests unitaires, car toutes les dépendances sont externalisées, ce qui permet de se concentrer sur l’essentiel.

Si avec ça, l’eau ne vous monte pas à la bouche, je ne sais pas ce qu’il vous faut.


Explications

Injection de dépendances

Le principe est si simple qu’il y a de fortes chances que vous l’avez déjà utilisé sans vous en rendre compte. Voici comment ça se présente :

Il s’agit d’une classe avec un constructeur qui prend en paramètre le service et va le stocker dans un champ (privé de préférence).

C’est tout ?

Oui, pour le client en tout cas. La construction du service est déléguée au contexte (le code qui appelle le client) et peut effectivement s’avérer complexe.

Mais peu importe, car le client ne sera jamais impacté par ce qu’il s’y passe. Il est complètement découplé du contexte dans lequel il est appelé.


Injection et inversion

Attends une petite minute, sagouin. Tu m’as dit que mon client allait être générique, mais dans ton exemple, il n’accepte que SuperMarioBros !?

Vous avez raison. Quand on met une cartouche dans la NES, elle ne sait pas à quel jeu s’attendre.

Pour reproduire le même effet, il suffit de mélanger l’injection de dépendances avec le principe d’inversion de dépendances (DIP).

Rien de plus simple, il suffit de

  1. Réunir tous les types que l’on souhaite passer à notre client sous une interface commune.
  2. De passer au client un objet du type de cette interface.

Ainsi, on laisse au contexte le choix du type du service, et le client n’y voit que du feu.

Le truc sympa, c’est que ça permet au contexte de changer le comportement du client de l’extérieur.

What ??? Comment c’est possible ?

Grâce au polymorphisme ! Même si deux types héritent de la même interface, ils peuvent implémenter leurs méthodes différemment.

C’est comme pour la NES, d’une cartouche à l’autre, le résultat va être totalement différent, mais la console reste la même.


Application

Commençons par créer différents types et les placer sous la même interface (DIP).

public interface INesVideoGame
{
    void Run();
}

public class SuperMarioBros : INesVideoGame
{
    public void Run() => Console.WriteLine("Playing Super Mario Bros");
}

public class Contra : INesVideoGame
{
    public void Run() => Console.WriteLine("Playing Contra");
}

public class DeadlyTowers : INesVideoGame
{
    public void Run() => Console.WriteLine("Suffering on Deadly Towers");
}
L’opérateur => n’est qu’une astuce de C# pour simplifier les fonctions à une seule ligne.

Ensuite, créons notre client. Celui-ci possède bien sûr un constructeur, mais aussi une méthode Start() qui lui permettra de lancer le jeu injecté.

public class NintendoEntertainmentSystem
{
    private INesVideoGame VideoGame;

    public NintendoEntertainmentSystem(INesVideoGame cartridge) =>
        VideoGame = cartridge;

    public void Start() => VideoGame?.Run();
}
L’opérateur ? n’est qu’une astuce de C# pour éviter d’avoir à vérifier si l’objet est null avant de l’utiliser.

Et voilà, quick and simple, maintenant, il ne reste plus qu’à l’utiliser.

static void Main(string[] args)
{
    NintendoEntertainmentSystem nes;

    var superMarioBrosCartridge = new SuperMarioBros();
    nes = new NintendoEntertainmentSystem(superMarioBrosCartridge);
    nes.Start();    // Playing Super Mario Bros

    var contraCartridge = new Contra();
    nes = new NintendoEntertainmentSystem(contraCartridge);
    nes.Start();    // Playing Contra

    var deadlyTowersCartridge = new DeadlyTowers();
    nes = new NintendoEntertainmentSystem(deadlyTowersCartridge);
    nes.Start();    // Suffering on Deadly Towers
}
Même objet, même méthode, et pourtant, un comportement différent juste parce qu’on a changé le type du service.

Dans cet exemple, le contexte est la fonction Main() d’une application console, mais on peut en imaginer d’autres, par exemple :

  • Une API REST.
  • L’évènement de clic sur un bouton.
  • Un job qui se lance toutes les 24H (pratique pour récupérer ses récompenses quotidiennes, toi-même tu sais).

Vous retrouverez le code source de ce programme ici.


Cas d’utilisation

L’injection de dépendances est pertinente lorsque vous voulez :

  • Avoir un contrôle total sur la configuration d’un service.
  • Altérer le comportement d’un client de l’extérieur.
  • Réduire le nombre de paramètres d’une fonction.

Mais attention, je vous ai peut-être donné l’impression que les contrôles directs sont mauvais, et qu’il ne faut jamais les utiliser. Ce n’est pas tout à fait vrai.

Un gros désavantage de l’injection de dépendances (et de l’inversion des contrôles de manière générale) et qu’il se fait au détriment de l’encapsulation.

Ainsi, je recommande de NE PAS l’utiliser quand :

  • Votre service est construit sans paramètre extérieur. Typiquement, les objets locaux et/ou temporaires.
  • Vous voulez limiter l’impact du contexte sur le service. Par exemple : si certaines données ne doivent pas être altérées hors du client.

Et j’ai beau râler sur Nintendo, c’est probablement pour ça qu’ils ont bloqué le catalogue de la NES Mini. Donc respect à eux, même si cette console, c’est quand même sacrément de la…


Voilà pour cet article sur l’injection de dépendances. Un principe très connu, mais une piqure de rappel ne fait jamais de mal.

Pour ceux qui ont du temps à perdre, vous pouvez aller lire mes notes additionnelles pour en apprendre encore plus de trucs sur le sujet.

Soutenez le blog en partageant cet article (merci 💙) et suivez-moi sur Twitter @ITExpertFr. Étant donné que je ne tweet qu’une fois tous les 36, je ne risque pas d’envahir votre TL 😁

Et pour les amateurs d’architecture logicielle et de patrons de conception, voici une paire d’articles à vous mettre sous la dent :

Quant à moi, je vous laisse, tout ça m’a donné envie de ressortir Super Mario 64, donc je pars me faire une petite game.

À bientôt pour un nouvel article sur le blog des développeurs ultra-efficaces. Tchao !


Accéder au bonus : Notes inintéressantes sur l’injection de dépendances.