Dernière mise à jour:

Le principe d'injection de dépendance

Cédric Gérard
Cédric Gérard Code

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.

        
class UserService {
  private database: Database;

constructor() { this.database = new Database(); }
getUsers(): User[] { return this.database.query('SELECT * FROM users'); }
saveUser(user: User): void { this.database.insert('users', user); } }
TypeScript

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 :

        
class UserService {
  private database: Database;

constructor(database: Database) { this.database = database; }
getUsers(): User[] { return this.database.query('SELECT * FROM users'); }
saveUser(user: User): void { this.database.insert('users', user); } }
TypeScript

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 :

        
class UserService
  def initialize(database)
    @database = database
  end

def get_users @database.query('SELECT * FROM users') end
def save_user(user) @database.insert('users', user) end end
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.

Cédric Gérard

Cédric Gérard

Je suis dans l'informatique depuis tout jeune. D'abord intéressé par le hardward (montage, overcloking), j'ai mis du temps à trouver ma voie. Je suis tombé dans le développement en 2007, je n'ai jamais arrêté depuis..

Aujourd'hui, je suis développeur web avec une plus grande appétence pour le backend. J’accorde beaucoup d’attention à la valeur apportée aux utilisateurs finaux. On ne réalise pas d'application que pour se faire plaisir, après tout.

Je mets aussi un point d'honneur à livrer du code de qualité en m'appuyant sur les bonnes pratiques du développement logiciel et je défends les valeurs du software craftmanship.

L'agilité est également un élément essentiel pour un travail fiable et efficace. Je ne parle pas de méthode, mais de l'état d'esprit prôné par l'agilité.

J'aime partager mes compétences et j'ai une appétence particulière pour l'encadrement des développeurs juniors.

Je suis également en quête de sens, aucune technologie étant une fin en elle-même, j'ai besoin de savoir pourquoi je travaille et qu'elle est la valeur produite.

Articles en relation

Le principe de ségrégation des interfaces

Le principe de substitution de Liskov

Le principe Ouvert/Fermé