L'injection de dépendances arrive rarement au bon moment dans un parcours d'apprentissage. Elle apparaît en général dans la documentation d'un framework, entre une histoire de conteneur et une histoire d'annotations, à un stade où le lecteur cherche surtout à afficher une page qui fonctionne.
Résultat, le terme reste associé à de la plomberie de framework. Alors qu'il désigne un geste de programmation qui tient en une ligne et qui existait bien avant Symfony ou Spring.
Ce décalage explique la plupart des blocages. On apprend le vocabulaire du conteneur avant d'avoir rencontré la douleur que l'injection soulage, et sans cette douleur, la solution ressemble à de la complication gratuite.
Cet article prend le chemin inverse : d'abord le code qui coince, ensuite le geste qui le débloque, puis les conteneurs, et enfin les erreurs qui font abandonner en route. Les exemples sont en JavaScript et en Python, mais le principe se transpose tel quel en PHP, en Java ou en C#.
Un code qui marche et qu'on ne peut pas faire évoluer
Voici une classe qui envoie un email de confirmation après une commande. Elle est écrite de la façon la plus directe qui soit, celle qu'on apprend en premier.
class ServiceCommande {
validerCommande(commande) {
const mailer = new MailerGmail() // fabriqué à l'intérieur
mailer.envoyer(commande.email, "Commande validée")
}
}
Ce code fonctionne. Il passe en production. Il rend service pendant des mois. Les ennuis commencent le jour où quelque chose doit changer autour de lui, et ce jour arrive toujours plus tôt qu'on ne l'imagine.
Changer de fournisseur d'envoi impose de rouvrir la classe. Écrire un test impose d'envoyer un vrai email à chaque exécution, ce qui rend le test lent, dépendant du réseau, et franchement dangereux si une adresse de client traîne dans les données de test. Ajouter un mode "brouillon" qui n'envoie rien impose un drapeau conditionnel au milieu de la logique métier.
Le point commun de ces trois blocages tient dans un mot : couplage. La classe ne réclame pas un moyen d'envoyer un email, elle décide que ce sera Gmail. Elle tranche une question qui ne relève pas de sa responsabilité, et elle la tranche définitivement, à l'intérieur d'une méthode.
Multiplié par toutes les classes d'un projet, ce schéma produit une application où chaque fichier décide seul de ses outils. Le remplacement d'une brique transverse, une nouvelle façon d'accéder à la base par exemple, se transforme alors en chantier de plusieurs jours réparti sur des dizaines de fichiers.
Deux morceaux de code sont couplés quand modifier l'un oblige à toucher l'autre. Un couplage fort rend le code rigide : chaque évolution se propage. Un couplage faible laisse remplacer une pièce sans démonter les voisines.
Le geste tient en un paramètre
L'injection de dépendances se résume à une règle : une classe ne fabrique pas les objets dont elle a besoin, elle les reçoit. Le mailer n'est plus créé à l'intérieur, il entre par le constructeur.
class ServiceCommande {
constructor(mailer) { // la dépendance entre ici
this.mailer = mailer
}
validerCommande(commande) {
this.mailer.envoyer(commande.email, "Commande validée")
}
}
// Le choix se fait à l'assemblage, hors de la classe
const service = new ServiceCommande(new MailerGmail())
Une ligne a bougé. La classe ignore désormais quel mailer elle manipule, et c'est l'objectif recherché plutôt qu'un effet secondaire. Elle demande un objet capable d'envoyer un email, sans se prononcer sur l'identité du fournisseur.
Le mot injection décrit le fait de faire entrer la dépendance depuis l'extérieur. Le mot dépendance désigne l'objet sans lequel la classe ne peut pas travailler. Une fois traduit, le vocabulaire savant recouvre une opération élémentaire : passer en paramètre au lieu de fabriquer sur place.
Une image qui aide en formation : un appareil photo dont les piles seraient soudées à l'intérieur deviendrait un presse-papier dès qu'elles tombent à plat. Le compartiment à piles, c'est l'injection de dépendances appliquée au matériel.
Le bénéfice qui rend le principe évident : les tests
Sur la première version, tester la validation d'une commande revenait à envoyer un email à chaque exécution du test. Inutilisable en pratique.
Avec la dépendance passée de l'extérieur, le test fournit un faux mailer, un objet qui enregistre ce qu'on lui demande sans rien envoyer nulle part.
class MailerFactice {
constructor() { this.envois = [] }
envoyer(dest, sujet) { this.envois.push({ dest, sujet }) }
}
const faux = new MailerFactice()
const service = new ServiceCommande(faux)
service.validerCommande({ email: "client@exemple.fr" })
console.assert(faux.envois.length === 1)
console.assert(faux.envois[0].sujet === "Commande validée")
Le test s'exécute en quelques millisecondes, sans réseau, sans compte de messagerie, sans risque d'écrire à un client réel. Il vérifie ce qui compte : que le service demande bien un envoi, avec le bon destinataire et le bon sujet.
C'est là que le déclic se produit chez la plupart des personnes qui apprennent. Tant que l'injection reste une histoire de propreté du code, l'argument glisse. Quand elle devient la condition pour écrire un test qui tourne en local sans rien casser, la règle s'ancre. Le sujet des tests est développé dans l'article sur les tests et le TDD pour sortir de la peur de toucher au code qui marche.
Trois portes d'entrée pour une dépendance
Une dépendance peut entrer dans une classe de trois manières, et les trois se croisent dans du code réel. Le choix n'est pas cosmétique : il détermine si une dépendance est obligatoire ou facultative, et à quel moment de la vie de l'objet elle devient disponible.
- Par le constructeur : la dépendance est fournie à la création de l'objet. La plus répandue, parce qu'elle rend la dépendance obligatoire, visible dans la signature, et impossible à oublier.
- Par un setter : la dépendance arrive après la création, via une méthode dédiée. Adaptée aux dépendances optionnelles ou remplacées en cours de vie de l'objet.
- Par la méthode : la dépendance est passée directement à la fonction qui l'utilise, pour ce seul appel. Utile quand elle ne sert nulle part ailleurs dans la classe.
Mon avis, et il n'est pas neutre : le constructeur devrait être le choix par défaut, et les deux autres des exceptions justifiées. Un objet dont les dépendances arrivent après coup peut exister dans un état incomplet, et ce genre de fenêtre finit toujours par produire un bug difficile à reproduire. Spring a d'ailleurs fait ce virage officiel en recommandant l'injection par constructeur plutôt que par champ, après des années de tolérance sur les annotations posées directement sur les attributs.
Dépendre d'un contrat plutôt que d'une classe
Le geste peut aller un cran plus loin. Plutôt que d'attendre une classe précise, la classe attend un contrat : une liste de méthodes qu'un objet promet de fournir, sans préciser comment il s'y prend. En programmation orientée objet, ce contrat s'appelle une interface.
from abc import ABC, abstractmethod
class Mailer(ABC):
@abstractmethod
def envoyer(self, dest: str, sujet: str) -> None: ...
class MailerSmtp(Mailer):
def envoyer(self, dest: str, sujet: str) -> None:
... # envoi réel
class ServiceCommande:
def __init__(self, mailer: Mailer): # le contrat, pas l'implémentation
self.mailer = mailer
def valider(self, email: str) -> None:
self.mailer.envoyer(email, "Commande validée")
Un envoi SMTP, un service transactionnel, un faux mailer de test : tous remplissent le contrat, tous sont acceptés, et la classe ne fait aucune différence entre eux. Cette orientation porte un nom, l'inversion de dépendance, le D des principes SOLID.
Attention à ne pas fusionner les deux notions. L'injection décrit un mode de passage, l'inversion décrit vers quoi on pointe. On peut injecter une classe concrète sans aucune interface, et le code sera déjà plus testable. On peut aussi définir des interfaces partout et continuer à instancier en dur à l'intérieur des classes, ce qui annule le bénéfice.
Pour situer cette règle parmi les autres, LaPolaris publie un hub qui rassemble vingt principes de code maintenable, avec un article dédié par principe.
Et qui fabrique les objets, alors ?
Si chaque classe reçoit ses dépendances, quelqu'un doit bien les créer au sommet. Sur un petit projet, l'assemblage se fait à la main dans le fichier de démarrage : on instancie le mailer, on le passe au service, on branche le service à la route.
La méthode manuelle atteint sa limite quand les dépendances s'emboîtent sur plusieurs niveaux. Le service a besoin d'un mailer, qui a besoin d'une configuration, qui a besoin de lire des variables d'environnement, et il faut remonter cette chaîne à chaque point d'entrée de l'application.
Le conteneur d'injection de dépendances automatise ce câblage. On lui déclare une fois comment fabriquer chaque pièce, il se charge de l'ordre et des branchements. Symfony repose entièrement sur le sien et pratique l'autowiring par défaut depuis la version 3.3, ce qui lui permet de déduire les dépendances à partir des types déclarés dans le constructeur. FastAPI propose son système via Depends, avec la même logique appliquée aux fonctions de route.
Ce confort a un revers pédagogique : quand le framework câble tout seul, on peut écrire du code injecté pendant des mois sans avoir compris ce qui se passe. Écrire l'assemblage à la main sur un petit projet reste le meilleur moyen de voir le mécanisme. Ces conteneurs sont manipulés dans nos parcours backend, en PHP avec la formation Symfony 7, en Java avec la formation Spring Boot pour les API REST, ou en Python avec la formation FastAPI.
Les erreurs qui donnent une mauvaise réputation au principe
Les critiques adressées à l'injection de dépendances visent le plus souvent des usages abusifs plutôt que le principe. Quatre reviennent constamment.
- Injecter par réflexe, y compris l'inutile
Une fonction de calcul sans contact avec l'extérieur n'a aucune dépendance à recevoir. Ce qui mérite d'être injecté touche le monde réel : base de données, réseau, système de fichiers, horloge, générateur aléatoire. - Un constructeur à dix paramètres
L'injection ne crée pas ce symptôme, elle le rend visible. Une classe qui réclame huit collaborateurs assume trop de responsabilités et demande à être découpée. - Injecter le conteneur lui-même
Passer le conteneur entier à une classe pour qu'elle pioche dedans la recouple à toute l'application et masque ses vraies dépendances. Ce contre-modèle porte un nom, le service locator, et il annule le bénéfice recherché. - Créer une interface pour chaque classe
Une interface avec une seule implémentation qui n'a aucune chance d'en avoir une deuxième ajoute un fichier et zéro souplesse. L'abstraction se justifie quand une variation existe ou se profile.
Ces erreurs partagent une racine commune : traiter l'injection comme une case à cocher d'architecture au lieu d'un moyen de réduire le couplage. La question à se poser devant chaque dépendance reste la même, va-t-elle changer ou devoir être simulée un jour. La répartition des responsabilités entre objets est aussi le sujet de l'article sur Tell Don't Ask et les objets actifs.
Par où commencer demain
Prends une classe existante de ton projet qui instancie quelque chose à l'intérieur d'une méthode. Sors cette instanciation, passe l'objet par le constructeur, et écris un test avec un remplaçant factice. Une classe, une heure, et le principe devient une habitude plutôt qu'une définition.
Le reste, interfaces, conteneurs, autowiring, viendra quand un besoin le réclamera. Ce sera peut-être dans six mois, ou dès la prochaine fois qu'un test refusera de tourner en local.