Questions fréquentes sur la DI (FAQ)
La DI est-elle un autre nom pour l'IoC ?
L'Inversion of Control (IoC) est un principe qui décrit le flux de contrôle dans un programme : est-ce votre code
qui appelle du code externe, ou est-ce du code externe (comme un framework) qui appelle votre code ? L'IoC est un concept large
qui englobe les événements, le fameux principe d'Hollywood et d'autres aspects. Ce concept
comprend aussi les factories, dont parle la Règle n° 3 :
laissez faire la factory, qui représentent une inversion de l'opérateur new.
L'injection de dépendances (DI) s'intéresse à la façon dont les objets obtiennent leurs dépendances (c'est-à-dire les autres objets dont ils ont besoin pour travailler). C'est un patron de conception qui prône de passer les dépendances aux objets de manière explicite, plutôt que de laisser les objets les créer ou les chercher.
La DI peut donc être considérée comme une forme particulière d'IoC. Toutes les formes d'IoC ne favorisent cependant pas un code propre. Font par exemple partie des anti-patterns les techniques reposant sur l'état global ou sur le patron Service Locator.
Qu'est-ce qu'un Service Locator ?
C'est une approche alternative à l'injection de dépendances. Elle repose sur un objet central (le locator) où sont enregistrés tous les services (dépendances) disponibles. Lorsqu'un objet a besoin d'une dépendance, il la demande au Service Locator.
Comparé à la DI, il manque toutefois de transparence. Les dépendances sont cachées dans le code de l'objet (les appels au locator) au lieu d'être explicites dans son API (constructeur ou méthodes), et il faut donc inspecter le code pour comprendre les liens. Les tests sont eux aussi plus compliqués : vous ne pouvez pas simplement passer des dépendances simulées lors de l'instanciation d'un objet, il faut souvent manipuler le Service Locator lui-même. De plus, le Service Locator introduit une dépendance inutile : les objets deviennent liés au locator, contrairement à la DI, où les objets ignorent idéalement l'existence du conteneur.
Quand vaut-il mieux ne pas utiliser la DI ?
Il n'existe pas d'inconvénient notable connu à utiliser correctement le patron de conception de l'injection de dépendances. Au contraire, obtenir les dépendances depuis des emplacements accessibles globalement (comme des propriétés statiques ou des singletons) entraîne de nombreuses complications, tout comme l'utilisation d'un Service Locator. Utiliser la DI est donc pratiquement toujours recommandé. Ce n'est pas un dogme : simplement, aucune meilleure alternative pour gérer proprement les dépendances ne s'est imposée.
Il existe cependant des situations particulières et limitées où un accès global aux objets peut être acceptable. Par exemple pendant le débogage, quand vous avez besoin de dumper la valeur d'une variable, de mesurer un temps d'exécution ou de journaliser un message à un endroit précis. Dans ces cas, qui concernent des actions temporaires qui seront ensuite retirées du code, l'utilisation d'un dumper, d'un chronomètre ou d'un logger accessible globalement peut être légitime. Ces outils ne font pas partie de la conception même de l'application.
L'utilisation de la DI a-t-elle des inconvénients ?
L'injection de dépendances apporte-t-elle des désavantages, comme un effort d'écriture accru ou des performances réduites ? Que perdons-nous en commençant à écrire du code conforme à la DI ?
La DI elle-même a un impact négligeable sur les performances d'exécution ou sur la consommation mémoire. Les performances du conteneur DI peuvent jouer un rôle, mais Nette DI compile le conteneur en simple code PHP, si bien que la surcharge pendant l'exécution de l'application est pratiquement nulle.
Quand vous écrivez du code selon les principes de la DI, vous devez souvent créer des constructeurs qui reçoivent les dépendances. Ce qui pouvait sembler fastidieux autrefois est devenu très rapide grâce aux IDE modernes et à des fonctionnalités comme la promotion des propriétés du constructeur de PHP 8. Les factories peuvent souvent être générées automatiquement par Nette DI, ce qui réduit encore le code répétitif. En contrepartie, vous n'avez plus à écrire de singletons ni d'accesseurs statiques.
Globalement, une application bien conçue utilisant la DI n'est ni sensiblement plus courte ni sensiblement plus longue qu'une application reposant sur des singletons ou sur un accès global. Le code lié à la création et au câblage des dépendances est simplement déplacé des différentes classes vers des endroits dédiés : la configuration du conteneur DI et les factories.
Comment réécrire une application legacy en DI ?
La migration d'une application legacy vers l'injection de dépendances peut être un processus exigeant, en particulier pour les applications volumineuses et complexes. Il est important d'aborder ce processus de manière systématique.
- Lors du passage à l'injection de dépendances, il est important que tous les membres de l'équipe comprennent les principes et les pratiques employés.
- Analysez d'abord l'application existante pour identifier les composants clés et leurs dépendances. Établissez un plan indiquant quelles parties seront refactorisées et dans quel ordre.
- Implémentez un conteneur DI ou, mieux encore, utilisez une bibliothèque existante comme Nette DI.
- Refactorisez progressivement les parties de l'application pour qu'elles utilisent l'injection de dépendances. Cela peut impliquer de modifier des constructeurs ou des méthodes afin qu'ils reçoivent les dépendances en paramètres.
- Mettez à jour le code où les objets sont instanciés pour les récupérer depuis le conteneur ou utiliser les factories fournies par celui-ci.
N'oubliez pas que le passage à l'injection de dépendances est un investissement dans la qualité du code et dans la maintenabilité à long terme de l'application. Même s'il peut être difficile d'opérer ces changements, le résultat devrait être un code plus propre, plus modulaire et facilement testable, prêt pour de futures extensions et pour la maintenance.
Pourquoi la composition est-elle préférée à l'héritage ?
L'utilisation de la composition est généralement préférée à l'héritage pour réutiliser du code, car elle conduit à un couplage plus lâche. Avec la composition, vous risquez moins de rencontrer des problèmes où la modification d'une classe de base casse les sous-classes qui en dépendent. Un exemple typique est la situation appelée constructor hell.
Peut-on utiliser Nette DI Container en dehors de Nette ?
Absolument. Nette DI Container fait partie de Nette, mais il est conçu comme une bibliothèque autonome, utilisable indépendamment des autres parties du framework. Il suffit de l'installer avec Composer, de créer un fichier de configuration définissant vos services, puis d'écrire quelques lignes de code PHP pour créer le conteneur DI. Et vous pouvez immédiatement commencer à profiter de l'injection de dépendances dans vos projets.
Le chapitre Nette DI Container décrit un cas d'utilisation concret avec des exemples de code.
Pourquoi la configuration est-elle dans des fichiers NEON ?
NEON est un langage de configuration simple et facilement lisible, développé au sein de Nette pour configurer les applications, les services et leurs dépendances. Comparé à JSON ou YAML, il offre pour cet usage des possibilités bien plus intuitives et souples. En NEON, vous pouvez décrire naturellement des définitions de services et des relations qu'il serait difficile, voire impossible, d'exprimer aussi clairement en JSON ou en YAML.
L'analyse des fichiers NEON ralentit-elle l'application ?
Même si les fichiers NEON s'analysent très rapidement, leur vitesse d'analyse n'a en pratique aucune importance en production. En effet, les fichiers de configuration ne sont analysés qu'une seule fois, au premier lancement de l'application (ou lorsqu'ils changent). Après l'analyse, le code du conteneur DI est généré, mis en cache (stocké sur disque), et c'est ce code PHP compilé qui est exécuté à chaque requête suivante, sans qu'aucune analyse supplémentaire soit nécessaire.
C'est ainsi que cela se passe en environnement de production. Pendant le développement, les fichiers NEON sont analysés chaque fois que leur contenu change, ce qui garantit au développeur un conteneur DI toujours à jour. Comme nous l'avons dit, l'analyse elle-même est très rapide.
Comment accéder dans ma classe aux paramètres du fichier de configuration ?
Gardez à l'esprit la Règle n° 1 : laissez-vous les passer. Si une classe a besoin d'une information du fichier de configuration, n'essayez pas de trouver comment la classe pourrait aller la chercher. Demandez-la simplement, par exemple via le constructeur de la classe. Puis fournissez cette valeur dans le fichier de configuration.
Dans cet exemple, %myParameter% est un placeholder pour la valeur du paramètre myParameter, qui sera
passée au constructeur de MyClass :
# config.neon
parameters:
myParameter: Some value
services:
- MyClass(%myParameter%)
Si vous voulez passer plusieurs paramètres ou utiliser l'autowiring, il est utile d'encapsuler les paramètres dans un objet.
Nette prend-il en charge l'interface Container de PSR-11 ?
Nette DI Container ne prend pas en charge PSR-11 directement. Cependant, si vous avez besoin d'interopérabilité entre Nette DI Container et des bibliothèques ou frameworks qui attendent l'interface Container de PSR-11, vous pouvez créer un adaptateur simple qui servira de pont entre Nette DI Container et PSR-11.
Que signifient les termes conteneur, compiler, définition, etc. ?
Un petit lexique des mots qui reviennent sans cesse autour de Nette DI, la plupart lors de l'écriture d'extensions :
- Container (conteneur) – l'objet compilé (
Nette\DI\Container) qui crée les services à la demande et les conserve à l'exécution. Il est généré une seule fois sous forme de code PHP optimisé. - Compiler – la machinerie qui transforme les fichiers de configuration et les extensions en cette classe de conteneur.
- ContainerBuilder – le modèle modifiable du conteneur utilisé pendant la compilation ; il contient les définitions de services avant qu'aucun service réel n'existe. Voir Créer des extensions.
- Service – un objet géré par le conteneur, généralement créé une seule fois et partagé (un singleton) – une connexion à la base de données, un mailer, un logger.
- Definition (définition) – la recette d'un service : son type, la façon de le créer et ce qu'il faut faire ensuite. Nette transforme les définitions en méthodes fabriques du conteneur ; il en existe plusieurs sortes (voir types de définitions).
- Type – la classe ou l'interface d'un service, utilisée par l'autowiring pour associer les services aux endroits qui les réclament.
- Autowiring – le passage automatique des services aux constructeurs et aux méthodes selon leur type, pour que vous n'ayez pas à câbler les dépendances à la main.
- Tag – une étiquette attachée à une définition (éventuellement avec une valeur) ; une extension peut ensuite
retrouver tous les services qui la portent avec
findByTag(). - Setup – les appels supplémentaires effectués sur un service juste après sa création – appels de méthodes ou
affectations de propriétés, ajoutés avec
addSetup(). - Alias – un nom alternatif pour un service existant.