PHP
Injection de dépendance
Recherche…
Introduction
Constructeur Injection
Les objets dépendent souvent d'autres objets. Au lieu de créer la dépendance dans le constructeur, la dépendance doit être transmise au constructeur en tant que paramètre. Cela garantit qu'il n'y a pas de couplage étroit entre les objets et permet de modifier la dépendance à l'instanciation de classe. Cela présente un certain nombre d'avantages, notamment la lecture plus facile du code en rendant les dépendances explicites, et en simplifiant les tests car les dépendances peuvent être désactivées et simulées plus facilement.
Dans l'exemple suivant, Component
dépend d'une instance de Logger
, mais n'en crée pas. Il en faut un pour être passé en argument au constructeur.
interface Logger {
public function log(string $message);
}
class Component {
private $logger;
public function __construct(Logger $logger) {
$this->logger = $logger;
}
}
Sans injection de dépendances, le code ressemblerait probablement à:
class Component {
private $logger;
public function __construct() {
$this->logger = new FooLogger();
}
}
L'utilisation de new
pour créer de nouveaux objets dans le constructeur indique que l'injection de dépendance n'a pas été utilisée (ou n'a pas été utilisée de manière incomplète) et que le code est étroitement lié. C'est également un signe que le code est incomplètement testé ou peut avoir des tests fragiles qui font des hypothèses incorrectes sur l'état du programme.
Dans l'exemple ci-dessus, où nous utilisons l'injection de dépendance à la place, nous pourrions facilement passer à un autre enregistreur si cela devenait nécessaire. Par exemple, nous pouvons utiliser une implémentation Logger qui se connecte à un autre emplacement ou qui utilise un format de journalisation différent ou qui se connecte à la base de données plutôt qu'à un fichier.
Setter Injection
Les dépendances peuvent également être injectées par les installateurs.
interface Logger {
public function log($message);
}
class Component {
private $logger;
private $databaseConnection;
public function __construct(DatabaseConnection $databaseConnection) {
$this->databaseConnection = $databaseConnection;
}
public function setLogger(Logger $logger) {
$this->logger = $logger;
}
public function core() {
$this->logSave();
return $this->databaseConnection->save($this);
}
public function logSave() {
if ($this->logger) {
$this->logger->log('saving');
}
}
}
Ceci est particulièrement intéressant lorsque la fonctionnalité principale de la classe ne repose pas sur la dépendance au travail.
Ici, la seule dépendance nécessaire est la DatabaseConnection
, elle se trouve donc dans le constructeur. La dépendance Logger
est facultative et n'a donc pas besoin de faire partie du constructeur, ce qui facilite son utilisation.
Notez que lors de l'utilisation de l'injection par setter, il est préférable d'étendre la fonctionnalité plutôt que de la remplacer. Lors de la définition d'une dépendance, rien ne permet de confirmer que la dépendance ne changera pas à un moment donné, ce qui pourrait entraîner des résultats inattendus. Par exemple, un FileLogger
pourrait être défini au préalable, puis un MailLogger
pourrait être défini. Ce casse encapsulation et rend difficile de trouver des journaux, parce que nous remplaçons la dépendance.
Pour éviter cela, nous devrions ajouter une dépendance avec l'injection de setter, comme ceci:
interface Logger {
public function log($message);
}
class Component {
private $loggers = array();
private $databaseConnection;
public function __construct(DatabaseConnection $databaseConnection) {
$this->databaseConnection = $databaseConnection;
}
public function addLogger(Logger $logger) {
$this->loggers[] = $logger;
}
public function core() {
$this->logSave();
return $this->databaseConnection->save($this);
}
public function logSave() {
foreach ($this->loggers as $logger) {
$logger->log('saving');
}
}
}
Comme cela, chaque fois que nous utiliserons les fonctionnalités de base, elles ne seront pas endommagées même si aucune dépendance de l'enregistreur n'est ajoutée, et tout enregistreur ajouté sera utilisé même si un autre enregistreur a pu être ajouté. Nous étendons la fonctionnalité au lieu de la remplacer .
Injection de conteneur
L'injection de dépendance (DI) dans le contexte de l'utilisation d'un conteneur d'injection de dépendance (DIC) peut être considérée comme un sur-ensemble d'injection de constructeur. Un DIC analysera généralement les typeshints du constructeur d'une classe et résoudra ses besoins, en injectant efficacement les dépendances nécessaires à l'exécution de l'instance.
L'implémentation exacte dépasse largement le cadre de ce document mais, au fond, un DIC repose sur l'utilisation de la signature d'une classe ...
namespace Documentation;
class Example
{
private $meaning;
public function __construct(Meaning $meaning)
{
$this->meaning = $meaning;
}
}
... pour l'instancier automatiquement, en utilisant la plupart du temps un système de chargement automatique .
// older PHP versions
$container->make('Documentation\Example');
// since PHP 5.5
$container->make(\Documentation\Example::class);
Si vous utilisez PHP dans la version au moins 5.5 et que vous souhaitez obtenir le nom d'une classe de la manière indiquée ci-dessus, la méthode correcte est la seconde. De cette façon, vous pouvez trouver rapidement les utilisations de la classe en utilisant les IDE modernes, ce qui vous aidera beaucoup avec un éventuel refactoring. Vous ne voulez pas compter sur des chaînes régulières.
Dans ce cas, la Documentation\Example
sait qu’elle nécessite une Meaning
et un DIC instancie à son tour un type de Meaning
. L'implémentation concrète n'a pas besoin de dépendre de l'instance consommatrice.
Au lieu de cela, nous définissons des règles dans le conteneur, avant la création de l'objet, qui indique comment des types spécifiques doivent être instanciés, le cas échéant.
Cela présente de nombreux avantages, car un DIC peut
- Partager des instances communes
- Fournir une fabrique pour résoudre une signature de type
- Résoudre une signature d'interface
Si nous définissons des règles sur la manière dont un type spécifique doit être géré, nous pouvons obtenir un contrôle précis sur les types partagés, instanciés ou créés à partir d'une usine.