Le principe d'injection de dépendance
Le principe d’injection de dépendance (Dependency Injection ou DI) est le dernier principe SOLID. Il vise à réduire les dépendances directes entre les classes en permettant l’injection des dépendances nécessaires depuis l’extérieur.
Selon Bob Martin, le principe d’injection stipule :
Les modules de haut niveau ne doivent pas dépendre de modules de bas niveau. Les deux devraient dépendre des abstractions.
Les abstractions ne doivent pas dépendre des détails. Les détails devraient dépendre des abstractions.
Selon le principe DI, les dépendances d’une classe ne devraient pas être créées ou résolues à l’intérieur de cette classe elle-même. Au lieu de cela, les dépendances doivent être fournies de manière externe, généralement par le biais de constructeurs, de méthodes ou de propriétés, ce qui permet une plus grande flexibilité et facilite les tests et la réutilisabilité du code.
Grâce à cette méthode, les dépendances ne sont plus exprimées statiquement (hard coded ou instancié dans la classe) mais dynamiquement à l’exécution. Cela nous permet de changer la valeur d’une dépendance pendant que le programme s’exécute. Par exemple, on peut injecter un algorithme de recherche différent en fonction du choix de l’utilisateur. On peut aussi injecter une implémentation particulière en fonction du contexte d’exécution, par exemple un “fake” d’un service externe en local ou en environnement de test.
Prenons on exemple de code pour illustrer ce principe.
Dans cet exemple, la classe UserService
ne respecte pas le principe d’injection de dépendance. Elle crée directement une instance de la classe Database
à l’intérieur de son constructeur. On crée ici une dépendance étroite entre les deux classes, rendant difficile le remplacement ou la substitution de la classe Database
par une autre implémentation. De plus, cela complique la mise en place des tests unitaires, car nous ne pouvons pas facilement simuler ou substituer la classe Database
.
Voici tout simplement la version qui permet de résoudre ce problème :
Dans cet exemple, nous avons mis en place l’injection par constructeur. La classe UserService
reçoit une instance de Database
lors de la création d’une nouvelle instance. Cela permet de fournir une implémentation spécifique de Database
à la classe UserService
lors de son instanciation. Cette pratique facilite également les tests unitaires en permettant de fournir une instance mock de Database
lors des tests.
Notre exemple s’appuie sur un langage typé statiquement. Le type à injecter (Database) est très souvent une interface qui explicite le contrat attendu par la classe cliente (UserService).
Qu’en est-il pour un langage dynamiquement typé, pour lequel il n’est pas possible de définir une interface spécifique pour l’injection. Et bien dans ce cas, il est possible d’injecter n’importe quelle instance qui possède les mêmes méthodes que celles utilisées dans la classe cliente. Et c’est la responsabilité du développeur que le comportement soit correct.
Voici la version du code précédent en Ruby :
Conclusion
Ce principe favorise la flexibilité, la testabilité et la réutilisabilité du code en réduisant les dépendances directes entre les classes. Avec ce principe, une classe peut se concentrer sur sa responsabilité unique et n’a plus a créer les objets dont elle dépend. Il est la base l’inversion de contrôle, le concept qu’on retrouve dans toutes les architectures qui s’appuient sur le pattern Ports/Adapters.