En effet, un tel appel suppose que le nom de la classe devant être instanciée est codé en dur dans le code, et il est donc alors impossible de redéfinir dynamiquement la classe à postériori de l'écriture du code.
<?php class foo { public function doSomething() { $bar = new bar(); // il est impossible de remplacer "bar" par une classe qui en hérite, par exemple $bar->doOtherThing(); } }
Une première solution pour mettre en œuvre l'injection de dépendance est donc de définir des méthodes au niveau de la classe permettant d'injecter des objets qui auront été créés en dehors de la classe, là où l'utilisation de l'opérateur new
est autorisée.
Ces méthodes, regroupées sous la dénomination de setter
sont autant de points d'entrée disponibles dans le code de la classe lors de son exécution.
<?php class foo { protected $bar = null; public function setBar(bar $bar) { $this->bar = $bar; return $this; } public function doSomething() { $this->bar->doOtherThing(); } }
Les plus attentifs auront remarqué que la propriété bar
n'est jamais initialisée dans le code précédent, et en toute logique, il faudrait ajouter une gestion d'erreurs pour que si la méthode foo::doSomething()
est appelée alors que bar
est indéfinie, le développeur soit averti du problème.
Cela signifie également que l'utilisateur de la classe devra systématiquement penser à appeler la méthode foo::setBar()
avec l'argument adéquat avant d'utiliser foo::doSomething()
, et l'on peut être certain que cela ne sera pas systématiquement le cas, ce qui provoquera des bugs et l'exaspération de l'utilisateur.
Pour remédier à cela, il suffit de définir un constructeur jouant également le rôle de setter
, mais avec une gestion de la dépendance suffisamment intelligente pour ne pas nécessiter le recours systématique à foo::setBar()
, et pour cela, l'opérateur elvis, aka ?: et apparu avec PHP 5.3, est l'outil idéal.
<?php class foo { protected $bar = null; public function __construct(bar $bar = null) { $this->setBar($bar ?: new bar()); } public function setBar(bar $bar) { $this->bar = $bar; return $this; } public function doSomething() { $this->bar->doOtherThing(); } }
Pour rendre votre code encore plus modulaire, vous pouvez de plus recourir à une interface pour typer le type d'objet géré par l'objet.
De cette façon, toute classe implémentant l'interface requise pourra être utilisée par votre méthode.
<?php interface barInterface { public function doOtherThing(...); } class foo { protected $bar = null; public function __construct(barInterface $bar = null) { $this->setBar($bar ?: new bar()); } public function setBar(barInterface $bar) { $this->bar = $bar; return $this; } public function doSomething() { $this->bar->doOtherThing(); } }
Une fois cette stratégie mise en place au niveau de toute les classes de votre programme, vous êtes virtuellement à même d'injecter les classes de votre choix à tous les niveaux de votre code, et vous êtes de plus capable de le tester facilement :
<?php class foo extends atoum\test { public function testDoSomething() { $this ->if($foo = new foo(new \mock\bar())) ->and($bar->getMockController()->doOtherThing->throw = $exception = new \exception('Houston, we have a problem !') ->then ->exception(function() use ($foo) { $foo->doSomething(); }) ->isIdenticalTo($exception) ; } }
L'objectif est donc atteint, mais la méthode a cependant un inconvénient majeur.
En effet, la longueur de la signature du constructeur et le nombre de setter
vont évoluer proportionnellement au nombre de dépendances nécessaires au fonctionnement de la classe.
De plus, tout ajout de dépendance nécessitera, outre l'ajout du setter
correspondant, la modification de la signature du constructeur et donc potentiellement la mise à jour de l'ensemble des tests unitaires y faisant appel.
Cela peut donc vite devenir très lourd à concevoir et à maintenir, surtout dans le cadre d'un projet conséquent, et je parle d'expérience car le principe énoncé ci-dessus est utilisé abondamment dans atoum, mon framework de test unitaire simple, moderne et intuitif pour PHP 5.3+.
C'est d'ailleurs la raison pour laquelle j'ai décidé de modifier totalement mon approche et de faire dorénavant appel à un injecteur de dépendance pour rendre le code de atoum encore plus souple et plus facile à maintenir et à faire évoluer.
Grâce à cet injecteur, le code de notre classe foo
deviendrait le suivant :
<?php class foo { protected $bar = null; public function __construct(dependenciesInjector $injector = null) { $this->setDependencies($injector ?: new dependenciesInjector()); } public function setDependencies(dependenciesInjector $injector) { $injector['bar'] = $injector['bar'] ?: function() { return new bar(); }; $this->bar = $injector['bar'](); return $this; } public function doSomething() { $this->bar->doOtherThing(); } }
De cette façon, si à l'avenir j'ai besoin d'ajouter une dépendance, je ne dois modifier que la méthode foo::setDependenciesInjector()
et je n'ai plus besoin de mettre à jour tous les appels au constructeur dans mes tests unitaires, puisque je n'ai plus qu'à enrichir mon injecteur de dépendance avec les bouchons nécessaires pour qu'ils soient automatiquement utilisés.
<?php class foo extends atoum\test { public function testDoSomething() { $this ->if($injector = new injector()) ->and($injector['bar'] = new \mock\bar()) ->and($foo = new foo($injector)) ->and($bar->getMockController()->doOtherThing->trow = $exception = new \exception('Houston, we have a problem !') ->then ->exception(function() use ($foo) { $foo->doSomething(); }) ->isIdenticalTo($exception) ; } }
L'injection de dépendance est donc un concept puissant que je trouve indispensable à mettre en œuvre pour obtenir un code de qualité modulable et facile à tester et à maintenir.
Sa mise en œuvre peut être très simple, mais dans le cadre d'un projet important et/ou qui évolue de manière importante et récurrente, les solutions les plus simples ne sont pas forcément les plus adaptées à moyen terme, et il peut se révéler plus rentable d'utiliser un injecteur de dépendance qui apportera encore plus de souplesse à la fois lors de la conception du code et lors de sa mise en œuvre.
11 réactions
1 De laurentj - 28/06/2012, 18:14
J'ai toujours trouvé l'injection de dépendance, lourde avec un aspect bricolage.
Pour moi c'est le langage qui devrait implémenter des mécanismes d'injection de dépendance, et faire en sorte que cela soit le plus transparent possible pour le développeur.
Et puis à déboguer, c'est pas reluisant. Bonjour la stack d'appels, qui peut être remplie au final de nombreux appels à l'injecteur de dépendance. Bonjour les rapports de bug du type, "votre composant ne fonctionne pas", et passer des heures à essayer de trouver ce qui ne fonctionne pas, et découvrir au final que le mec a modifié chez lui la configuration des dépendances dans son application, avec un ensemble de composants qui n'arrivent pas à s'entendre.
Enfin bon, pour ceux qui aiment les projets bloatware, c'est sympa.
Tiens d'ailleurs, les getters/setters à la java, c'est bien bloatware ça aussi. Pourquoi faire un setBar alors que public $bar; suffit dans cet exemple. ça revient au même, c'est plus rapide à coder, et plus rapide à l’exécution. (je vois ça dans tellement de projets que ça m'énerve :-))
Vivement des vrais setters/getters à la javascript https://wiki.php.net/rfc/propertyge... , cela évitera de faire ces méthodes horribles quand il n'y en a pas besoin, et de rajouter des setters ou getters quand on veut, sans changer le code appelant.
Oui donc bref, tout ça c'est du plâtre sur une jambe de bois. Je ne comprend pas d'ailleurs que ça ne te choque pas, alors que tu répétais souvent que "PHP c'est de la merde"
2 De mageekguy - 29/06/2012, 09:51
@laurentj : J'ai longtemps partagé ton point de vue sur les injecteurs de dépendance, mais passé une certaine taille, il est vraiment délicat de s'en passer.
Certes, la mise en œuvre peut être délicate, en fonction de la solution retenue, mais avec le bon outil, à l'usage, ça reste utilisable, voir même agréable.
Concernant le débat
, si ta solution ne me choque pas en C/C++ ou tout autre langage disposant du typage fort vu que le compilateur ou la VM se charge alors du contrôle de type, dans un langage à typage faible comme PHP, je trouve que l'utilisation d'une propriété publique dangereuse, même si le permet en théorie de s'en passer.C'est cependant une préférence personnel, et ton approche est à mon avis meilleure techniquement parlant pour tout langage disposant du
.Mais de toute façon, même l'utilisation d'une propriété publique ne règle pas les problèmes soulevé par ce billet en terme de maintenance.
3 De Matthieu - 29/06/2012, 15:21
Je commence à utiliser de plus en plus moi aussi le principe d'injection de dépendance. Par contre je trouve que ça peut vite devenir fastidieux/polluant au niveau du code. L'idéal (de mon point de vue) serait plutôt une approche à la Spring avec l'injection réalisée par une "entité" supérieure qui manage les instances, et avec des annotations. Au niveau du code qu'on écrit, pas besoin de se soucier du coup des dépendances à injecter. Si tu as un avis là-dessus, voici une ébauche de mon projet pour le moment : https://github.com/mnapoli/PHP-DI
4 De mageekguy - 29/06/2012, 15:47
@Matthieu : Ta solution est trop complexe et gourmande en performance à mon goût, je préfère une API simple et efficace dans le code.
5 De madrid - 29/06/2012, 23:43
Bon alors si l'objectif est de limiter l'utilisation de l'opérateur «new», à quoi ça sert de faire «$injector['bar'] = $injector['bar'] ?: function() { return new bar(); }; » ?
Je trouve que ça complexifie le code pour déporter la dépendance. On peut faire pareil pour pas cher, ça se teste aussi bien et c'est bien plus lisible :
Pour ce qui est du chargement depuis un fichier de config, on peut bannir l'opérateur «new» et passer par des patterns factory qui font le job. Bien gaulé dans un gestionnaire dédié, c'est tout aussi efficace.
6 De mageekguy - 30/06/2012, 15:54
@madrid : si ton idée était de supprimer l'appel à ?: pour améliorer la lisibilité, c'est un peu loupé ;).
De plus, ta solution oblige le développeur à recourir systématiquement à
foo::getBar()
plutôt qu'à la propriété correspondante, ce qui alourdi le code et va là encore à l'encontre de ton objectif de départ.Et de toute façon, je n'ai pas dit qu'il y avait UNE bonne façon de faire, bien au contraire.
J'ai juste indiqué que mon expérience me permet de dire qu'à partir d'un certain
de code à tester et à maintenir, passer par un injecteur de dépendance devient rentable par rapport à des solutions .7 De Matthieu - 01/07/2012, 16:00
@mageekguy : L'objectif est de tendre vers l'injection à la spring. Un @Inject est ce qu'il y'a de plus simple. Et au niveau des performances, Doctrine-Annotation permet de mettre en cache très simplement le parsing des annotations, donc j'estime que ça n'est pas un problème (et de plus, "premature optimization is the root of all evil").
8 De mageekguy - 01/07/2012, 20:41
@Matthieu : c'est oublier un peu vite que "PHP is not Java" et qu'à ce titre, il ne fonctionne pas du tout de la même manière et que reprendre (j'insiste sur les guillemets) les idées du monde Java sans prendre en compte ce fait est la porte ouverte à tout un tas d'emmerdement.
Et il en est de même pour le cache.
Pour beaucoup, c'est une solution miracle, pour moi, c'est un cautère sur une jambe de bois qui pose plus de problèmes qu'il n'en résout, car il faut le générer, le gérer, et l'invalider, le tout aux bons moments, et c'est loin d'être simple.
Ce n'est pas pour rien que la phrase répétée vingt fois par jour par les développeurs symfony est
.9 De Grummfy - 01/07/2012, 22:43
Perso, je trouve l'injection de dépendance assez utile dans des projets nécessitant une certaines souplesse ou d'une certaines taille. Par contre, les dérivés de "code à la java" commence à être soullant, on est obligé dans la plupart du cas d'avoir recours a du cache sinon adieu les performances. Pour moi le code doit-être utilisable tel quel, sans cache, le cache vient à la fin pour améliorer les performance mais en aucun cas pour le rendre utilisable ...
Par contre, le seul regret c'est que à l'heure actuel les injecteur de dépendance ne génère que très rarement une pdpdoc convenable et les ide ont du mal pour l'autocompletion ce qui peut-être gênant ...
10 De Paul - 07/07/2012, 08:03
Le problème que je vois en faisant un object "Injector" c'est qu'au final en regardant le prototype de la fonction, on n'est pas capable de savoir quelles sont les dépendances utilisées et leur type ...
Sauf si on se mets à faire une classe injector par "groupement" de dépendances.
Je dis ca mais j'utilise énormément Pimple (http://pimple.sensiolabs.org/) mais justement souvent je me dis "bon ok mais quelles sont les dépendances que je peux surcharger ?"
11 De mageekguy - 07/07/2012, 18:29
@Paul : Malheureusement, il n'y a pas de solution parfaite, et il y a toujours un prix à payer, à un endroit ou un autre du code.
Encore que dans mon cas, les tests unitaires permettent de documenter le code et donc de minorer cet inconvénient.