Lors de la semaine avant Noël a eu lieu la 5è semaine pour la Combe du Lion Vert. Cette semaine est l’occasion pour les alchimistes d’aborder des sujets plus avancés, comme l’architecture, des ateliers autour du BDD, … en présence des différents coachs. C’est aussi l’occasion pour ces mêmes coachs d’échanger entre eux. Justement, lors de ces échanges, je me suis rendu compte que ma compréhension de l’inversion de contrôle (Inversion of Control ou IoC en anglais), l’injection de dépendances (Dependency Injection/DI) et l’inversion de dépendances (Dependency Inversion/DI) était parcellaire. Et qui plus est, ces notions, bien que souvent utilisées ensemble, elles sont indépendantes. Dans cet article, nous verrons ainsi que inversion de contrôle ne veut pas forcément dire inversion de dépendances ou injection de dépendances et réciproquement.
Pour commencer, nous pouvons nous intéresser à l’inversion de contrôle. Le premier terme auquel pensent les “javaistes” quand ils entendent parler d’IoC est Spring. Côté Java, ce framework a d’abord été connu et popularisé pour cette fonctionnalité. D’ailleurs, la page Spring indique : “IoC is also known as dependency injection (DI)”. Est-ce que cela veut dire que “IoC = DI”? Dans ce cas, pourquoi deux termes pour désigner la même chose? C’est en fait un raccourci abusif. Est-ce qu’il n’y a que Spring d’ailleurs qui fasse de l’IoC? Que désigne-t’on alors quand on parle d’IoC?
Normalement, le comportement d’un programme est défini à la compilation. Avec l’inversion de contrôle, c’est un framework qui tisse les liens entre les composants à l’exécution et qui détermine le comportement du programme. Les frameworks jouent le rôle de “squelette extensible”. On adapte le programme au framework et on laisse le pouvoir au framework de nous appeler. D’où l’idée d’inversion de contrôle. On utilise aussi l’expression “Le principe d’Hollywood” : “Don’t call us, we’ll call you”.
Un exemple d’IoC : les servlets. Les servlets sont des composants qui permettent d’ajouter de nouveaux fonctionnalités à un serveur. Le développeur écrit la servlet et traite la requête qui lui est envoyé. Il implémente alors la fonction doGet ou doPost en fonction du traitement qu’il veut gérer.
public MyServlet extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // Do something } }
Cependant, ce n’est pas le développeur qui gère le cycle de vie, c’est le serveur d’application (comme Apache Tomcat). On est donc typiquement dans un cas d’inversion de contrôle. Dans ce cas, on respecte un contrat. Le framework s’occupe de l’orchestration des différents composants. Et dans le cas de Spring, c’est lui qui réalise les injections de dépendances entre objets.
Mais alors, qu’est-ce donc que l’injection de dépendances? Prenons l’exemple d’une classe. Une classe a généralement besoin de plusieurs dépendances pour réaliser son action: envoi d’emails, base de données, …. Normalement, c’est la classe qui crée et gère les dépendances:
public class MyClass { private EmailService emailService; Client() { emailService = new EmailService("<SMTP ADDRESS>"); } public void do() { emailService.sendSpam(); } }
Cependant, on peut aussi choisir de ne pas laisser la classe gérer ces dépendances mais les lui fournir. Pourquoi faire ça? Pour éviter d’avoir une adhérence trop forte entre la classe et ces dépendances. En injectant ces dépendances, on peut construire de différentes façons la dépendance sans devoir changer la classe. Une bonne façon de respecter le Single Responsibility Principle (SRP): une classe ne devrait changer que pour une seule raison.
public class MyClass { private EmailService emailService; Client(EmailService emailService) { this.emailService = emailService; } public void do() { emailService.sendSpam(); } }
La dépendance peut d’ailleurs être fournie de plusieurs façons: via un constructeur, via un setter, …
Passons au dernier terme: l’inversion de dépendance. Si on regarde de nouveau le premier exemple de l’injection de dépendance, on remarque que la classe est obligée de se conformer à la manière dont la dépendance a été écrite. On retrouve donc ici le principe de l’architecture en couches classique.
L’architecture en couches
De même, si le contrat de la dépendance change, nous sommes forcées de changer la classe. On a donc un lien de la classe vers la dépendance.
Toujours pour satisfaire le SRP, on aimerait inverser la relation entre la classe et la dépendance, afin de rendre la classe moins sensible aux changements de la dépendance.
Il est séduisant de se dire que tout changement dans la dépendance n’entraîne pas de modification dans la classe. Cependant, un changement dans la classe oblige à modifier la dépendance. On rentre dans un cercle vicieux. C’est là que l’inversion de dépendance (Dependency Inversion) intervient. Le concept repose sur deux notions fortes:
- Les modules de plus haut niveau ne devraient pas dépendre des modules de plus bas niveau. Les deux devraient dépendre des abstractions.
- Les abstractions ne devraient pas dépendre des détails. Les détails devraient dépendre des abstractions.
La solution est donc de faire intervenir un contrat (représenté généralement par une interface) entre la classe et la dépendance.
En regardant ces définitions, on pourrait dire que “IoC = D.I. (Dependency Injection) = D.I (Dependency Inversion)”. Mais il n’en est rien.
On peut déjà remarquer que la définition de l’inversion de dépendances ne présume pas de la manière dont les dépendances sont fournies. Les seules contraintes sont que les dépendances doivent se conformer à une abstraction et que les modules utilisent ces abstractions. D’autres patterns existent autre que l’injection de dépendances pour fournir ces dépendances comme le Service Locator, par exemple
Service service = (Service) context.getBean("serviceA");
De même, on peut réaliser de l’injection de dépendances sans pour autant utiliser une abstraction. Si on reprend l’exemple précédent pour l’injection de dépendance, on voit bien que la classe manipule toujours une dépendance concrète et non une abstraction.
Enfin, on peut faire de l’injection de dépendances sans IoC. Il est toujours possible de faire le lien entre les classes à la main, dans le point d’entrée de l’application par exemple:
public class EntryPoint { public static void main(String[] args) { EmailService emailService = new EmailService("<SMTP ADDRESS>"); MyClass myClass = new MyClass(emailService); myClass.do(); } }
Enfin, l’IoC est avant tout un principe de design avant d’être une technique. De plus, il existe d’autres frameworks qui font de l’IoC sans pour autant faire de l’injection de dépendances. On pourra repenser à l’exemple des servlet!
Ainsi, IoC ≠ D.I. ≠ D.I. … CQFD!