L'injection de dépendances traîne une réputation de complexité qui vient presque entièrement des outils censés la mettre en œuvre, pas du principe lui même. Le principe tient en une phrase : un objet ne va pas chercher ce dont il a besoin, on le lui donne. L'injection de dépendances sans conteneur consiste à appliquer cette règle avec les seuls moyens du langage, des constructeurs et un endroit unique où tout est assemblé. Sur un projet de taille moyenne, cela suffit largement, cela se lit sans documentation, et cela produit exactement les bénéfices attendus : du code testable, des dépendances visibles et des composants remplaçables. Le conteneur devient utile plus tard, sur des projets plus gros, et il vaut mieux savoir pourquoi avant de l'installer.
Le principe, et ce qu'il règle réellement
Avant la technique, il faut être au clair sur le problème traité. L'injection de dépendances ne rend pas le code plus rapide ni plus court : elle rend explicite ce qui était implicite, et c'est de cette explicitation que découlent tous les bénéfices. Cette clarification prolonge directement le travail d'organisation décrit dans la rubrique création de site Internet.
Le problème des dépendances cachées
Une classe qui construit elle même sa connexion à la base, qui appelle une fonction globale de configuration ou qui instancie directement un client d'envoi de courriel a des dépendances que rien n'annonce. Il faut lire tout son code pour les découvrir, et il devient impossible de l'utiliser dans un contexte différent, un test par exemple, sans emporter tout l'environnement avec elle. Le symptôme classique est le test unitaire qui exige une base de données et une connexion réseau pour vérifier un calcul de trois lignes.
Rendre les besoins visibles dans la signature
La correction consiste à déclarer ces besoins dans le constructeur, avec leurs types. La signature devient alors la documentation de la classe : on sait immédiatement de quoi elle dépend, sans lire son implémentation. Ce simple déplacement a un effet secondaire très utile, il rend visible l'excès : une classe dont le constructeur demande sept collaborateurs fait manifestement trop de choses, et le constructeur devient un indicateur de conception qu'aucune convention de nommage ne fournit.
Dépendre d'un contrat, pas d'une implémentation
Le second mouvement consiste à typer ces paramètres par une interface plutôt que par une classe concrète, quand cela a du sens. La classe n'a alors plus besoin de savoir si les courriels partent par le serveur local ou par un service distant, et le remplacement devient possible sans la modifier. Cette abstraction ne doit pas être systématique : créer une interface pour un objet qui n'aura jamais qu'une implémentation ajoute du code sans rien apporter, et c'est l'excès le plus courant chez ceux qui découvrent le principe. La règle pratique consiste à n'extraire une interface qu'au moment où une seconde implémentation apparaît réellement, ou lorsqu'une doublure de test devient nécessaire, ce qui est déjà une seconde implémentation.
Ce que cela change pour les tests
Un objet dont toutes les dépendances arrivent par le constructeur se teste en lui passant des doublures, sans base de données, sans réseau et sans système de fichiers. Le test devient rapide, déterministe et lisible, et surtout il devient possible de tester les cas d'erreur, ce qui est presque toujours impossible autrement : simuler une panne de service distant est trivial avec une doublure et très pénible avec le vrai service. Cette facilité change ce qu'on écrit comme tests : on cesse de vérifier uniquement le chemin nominal, qui est rarement celui qui casse en production.
Ce que cela ne règle pas
L'injection de dépendances n'est pas une architecture. Elle ne dit rien du découpage en couches, de la place de la logique métier ou de la gestion des transactions, et un code mal découpé le reste après l'avoir adoptée. Elle a même un effet pervers connu : en rendant facile l'ajout d'un collaborateur, elle peut encourager la prolifération de petits objets sans que personne ne se demande si le découpage a du sens. C'est un outil, pas une méthode de conception.
Injection par constructeur, par méthode ou par propriété
Trois formes coexistent et n'ont pas la même valeur. L'injection par constructeur est celle qu'il faut utiliser presque partout : elle garantit qu'un objet n'existe jamais dans un état incomplet, et rend ses dépendances obligatoires visibles d'un coup d'œil. L'injection par méthode convient à une dépendance réellement facultative, ou à une donnée qui varie d'un appel à l'autre, et se limite à ce cas. L'injection par propriété publique, où l'on affecte les collaborateurs après construction, est à éviter : elle autorise un objet à moitié construit, déplace les erreurs vers l'exécution et supprime la garantie d'immuabilité que le constructeur apportait. Les propriétés promues du langage rendent d'ailleurs l'injection par constructeur si concise qu'il n'y a plus aucun argument de verbosité en faveur des autres formes.

Le point de composition, pièce centrale
Si personne ne va chercher ses dépendances, quelqu'un doit bien les créer. Cet endroit s'appelle le point de composition, et sa qualité décide de celle de tout le dispositif. Il s'inscrit naturellement dans la structure de projet décrite dans notre article sur la manière de structurer un projet PHP sans framework.
Un seul endroit, aussi près que possible du point d'entrée
Tout l'assemblage doit se faire au démarrage de l'application, dans un fichier unique appelé par le contrôleur frontal. Ce fichier lit la configuration, crée les objets de bas niveau, puis les objets qui en dépendent, jusqu'au point où l'application peut être lancée. La règle est stricte : nulle part ailleurs dans le code une classe ne doit être instanciée avec ses collaborateurs. Dès qu'une exception est faite, la dépendance redevient cachée, et le bénéfice disparaît pour cette portion du code.
Une fonction de fabrication par service
Un point de composition écrit d'un seul tenant devient rapidement illisible. Le découper en petites fonctions, une par service à construire, chacune appelant les précédentes, garde le fichier lisible et rend l'ordre de construction explicite. Ces fonctions ne prennent en général qu'un paramètre, la configuration, et retournent l'objet construit. Elles se relisent comme une recette, et un nouvel arrivant comprend en dix minutes de quoi l'application est faite.
Construire paresseusement ce qui coûte cher
Instancier au démarrage tous les services d'une application, y compris ceux qu'une requête donnée n'utilisera pas, gaspille du temps et ouvre des connexions inutiles. La solution sans conteneur consiste à passer une fonction de création plutôt que l'objet lui même, l'objet n'étant construit qu'au premier appel. Cette technique se réserve aux dépendances réellement coûteuses, connexion à une base, client d'un service distant, moteur de rendu, et n'a aucun intérêt pour un objet dont la construction est immédiate.
Éviter le registre global déguisé
Une dérive fréquente consiste à créer un objet unique contenant tous les services, puis à le passer partout. On croit faire de l'injection de dépendances, on a en réalité recréé une variable globale : chaque classe peut atteindre n'importe quoi, plus rien n'est explicite, et les tests redeviennent difficiles. Le signe qui ne trompe pas est une classe dont le constructeur demande le registre entier plutôt que les deux services dont elle a besoin.
Gérer la configuration proprement
La configuration est la seule chose que le point de composition a le droit de lire depuis l'environnement. Elle doit être chargée une fois, validée immédiatement, et transmise sous forme d'objets typés plutôt que de tableaux associatifs anonymes. Une erreur de configuration doit provoquer un arrêt immédiat et explicite au démarrage, jamais une erreur obscure au milieu d'une requête. Cette validation précoce coûte quelques dizaines de lignes et évite des heures de diagnostic sur des symptômes sans rapport apparent avec leur cause.
| Situation | Sans injection | Avec injection explicite |
|---|---|---|
| Connaître les dépendances d'une classe | Lire tout son code | Lire son constructeur |
| Tester un calcul isolé | Environnement complet requis | Doublures suffisantes |
| Remplacer un service tiers | Modifier chaque appelant | Une ligne au point de composition |
| Simuler une panne externe | Presque impossible | Trivial |
| Repérer une classe qui en fait trop | Aucun signal | Constructeur trop long |
| Démarrer une commande en ligne | Charger tout le contexte web | Composer le strict nécessaire |
Composer différemment selon le contexte d'exécution
Un projet a rarement un seul point d'entrée : il y a le site, des commandes en ligne, des tâches planifiées, parfois une interface de programmation. Chacun a besoin d'une partie seulement des services, et rien n'oblige à tout construire partout. Le découpage qui fonctionne consiste à écrire un fichier d'assemblage commun contenant les fabriques, puis un court fichier par point d'entrée qui n'appelle que celles dont il a besoin. Une commande d'import ne monte alors ni le moteur de gabarits ni la gestion de session, ce qui la rend plus rapide et surtout plus facile à diagnostiquer quand elle échoue. C'est aussi ce qui permet d'exécuter cette commande dans un environnement réduit, sans serveur web ni configuration d'affichage.
Les situations qui résistent
Quelques cas ne se plient pas naturellement à l'injection par constructeur, et savoir les traiter évite de renoncer au principe dès la première difficulté. Ils supposent en général que le chargement automatique des classes soit déjà en place, tel que décrit dans notre article sur la manière de mettre en place un autoload PSR-4 avec Composer.
Les dépendances circulaires
Deux objets qui ont besoin l'un de l'autre ne peuvent pas être construits par simple injection, et ce blocage est une bonne nouvelle : il signale presque toujours un défaut de conception. La résolution passe par l'extraction de ce que les deux partagent dans un troisième objet, ou par le renversement de l'une des deux relations, souvent au moyen d'un événement. Contourner le problème par une méthode d'affectation après construction revient à masquer le symptôme, et transforme un signal de conception utile en dette silencieuse que personne ne relira.
Les objets créés à l'exécution
Certains objets ne peuvent pas être construits au démarrage parce qu'ils dépendent de données de la requête, une commande, un panier, un utilisateur. On n'injecte pas ces objets, on injecte de quoi les créer, c'est à dire une fabrique. Cette fabrique reçoit ses propres dépendances au point de composition, et expose une méthode qui prend les données variables en paramètre. C'est la réponse standard, et elle évite la tentation de rendre accessible depuis partout le contexte de la requête, qui est l'une des dépendances cachées les plus difficiles à retirer une fois installée.
Le code hérité qui appelle des fonctions globales
Sur un projet existant, une bonne partie du code utilise des fonctions globales et des variables partagées, et tout convertir d'un coup n'est pas réaliste. La méthode progressive consiste à envelopper ces appels dans de petites classes, à injecter ces enveloppes, puis à remplacer leur contenu plus tard sans toucher aux appelants. On obtient immédiatement la testabilité, et la migration réelle peut attendre le moment opportun, voire ne jamais avoir lieu si le code concerné ne bouge plus. C'est la propriété la plus intéressante de cette démarche sur un projet ancien : elle apporte un bénéfice immédiat à chaque petite étape, sans exiger d'engagement sur la suite.
Les cadriciels qui imposent leur mécanisme
Sur un projet reposant sur une plateforme qui instancie elle même les objets, notamment les systèmes de gestion de contenu à points d'accroche, l'injection par constructeur n'est pas toujours possible pour les classes que la plateforme construit. La parade consiste à réduire ces classes à une coquille qui ne fait que déléguer à un objet correctement injecté, construit dans un point de composition propre au projet. La couche d'adaptation reste mince et le code métier conserve toutes ses propriétés.
Indices relatifs relevés avant et après conversion sur un projet de taille moyenne, base cent pour l'état initial. Le seul poste qui augmente est la taille du fichier d'assemblage, ce qui est le prix de la visibilité.
Quand un conteneur devient justifié
Rien de ce qui précède n'interdit d'utiliser un conteneur, et il existe des situations où il apporte réellement quelque chose. Le mauvais réflexe est d'en installer un par défaut, sans avoir rencontré le problème qu'il résout.
Le seuil du nombre de services
Tant que le point de composition tient dans un fichier de trois cents lignes qu'on relit sans effort, il n'y a aucune raison d'ajouter une dépendance. Au delà, l'assemblage manuel devient pénible, notamment parce qu'ajouter un paramètre à un service très utilisé oblige à modifier plusieurs fonctions de fabrication. C'est à ce moment, et pas avant, que l'autocâblage par introspection commence à faire gagner du temps.
Les environnements multiples
Un projet qui doit fonctionner avec des implémentations différentes selon l'environnement, et dont la liste de ces variations s'allonge, gagne à externaliser la configuration de l'assemblage plutôt qu'à multiplier les conditions dans le code. C'est un des rares cas où un fichier de définition déclaratif est plus lisible que du code, à condition de rester lui même simple.
Ce qu'un conteneur coûte toujours
Un conteneur rend l'assemblage moins lisible, puisqu'il n'existe plus d'endroit où l'on voit l'application se construire. Il déplace certaines erreurs de la compilation vers l'exécution, une dépendance manquante ne se manifestant qu'au moment où le service est demandé. Il ajoute enfin une dépendance de plus à maintenir, avec son propre cycle de vie. Ces coûts sont acceptables sur un gros projet et disproportionnés sur un petit.
Garder la possibilité de revenir en arrière
Le meilleur usage d'un conteneur est celui qui laisse le code applicatif ignorant de son existence : les classes continuent de recevoir leurs dépendances par constructeur, et seul le point de composition change de forme. Le jour où le conteneur pose problème, ou simplement où l'on souhaite en changer, la migration se limite à ce fichier. À l'inverse, un code qui appelle le conteneur depuis l'intérieur des classes lie le projet entier à cet outil, ce qui est précisément ce que l'injection de dépendances cherchait à éviter. Une règle simple permet de tenir cette frontière dans la durée : aucun fichier du dossier applicatif ne doit mentionner le nom du conteneur, et une recherche textuelle suffit à le vérifier automatiquement à chaque livraison.
Un critère de décision simple
La question à se poser n'est pas de savoir si un conteneur est une bonne chose dans l'absolu, mais si le point de composition manuel pose aujourd'hui un problème concret et identifiable. Tant que la réponse est non, l'ajouter revient à payer un coût certain contre un bénéfice hypothétique. Quand la réponse devient oui, la conversion est simple précisément parce que le code applicatif a été écrit sans supposer quoi que ce soit sur la manière dont ses dépendances lui parviennent. C'est là le vrai bénéfice de la démarche : elle ne ferme aucune porte, alors que l'ordre inverse, commencer par l'outil, en ferme plusieurs sans qu'on s'en aperçoive. Cette progression a un autre mérite : elle rend visible le moment exact où le câblage manuel devient pénible, signal beaucoup plus fiable pour introduire un conteneur qu'une recommandation lue quelque part.
💬 0 commentaires
✍️ Laisser un commentaire
Créez un compte gratuit ou connectez-vous pour commenter.