Qu'est-ce que l'injection de dépendances ?

Ce chapitre vous présente les pratiques de programmation de base que vous devez suivre lorsque vous écrivez une application. Il s'agit des principes fondamentaux nécessaires à l'écriture d'un code propre, compréhensible et facile à maintenir.

Si vous apprenez et suivez ces règles, Nette sera là pour vous à chaque étape. Il s'occupera des tâches routinières à votre place et vous offrira un maximum de confort, afin que vous puissiez vous concentrer sur la logique elle-même.

Les principes que nous allons exposer ici sont très simples. Vous n'avez pas à vous soucier de quoi que ce soit.

Vous vous souvenez de votre premier programme ?

Nous ne savons pas dans quel langage vous l'avez écrit, mais s'il s'agit de PHP, il aurait pu ressembler à ceci :

function addition(float $a, float $b): float
{
	return $a + $b;
}

echo addition(23, 1); // imprime 24

Quelques lignes de code triviales, mais tellement de concepts clés cachés en elles. Qu'il y a des variables. Que le code est décomposé en unités plus petites, qui sont des fonctions, par exemple. Qu'on leur passe des arguments d'entrée et qu'elles renvoient des résultats. Il ne manque que les conditions et les boucles.

Le fait qu'une fonction prenne des données en entrée et renvoie un résultat est un concept parfaitement compréhensible, qui est également utilisé dans d'autres domaines, tels que les mathématiques.

Une fonction a sa signature, qui se compose de son nom, d'une liste de paramètres et de leurs types, et enfin du type de la valeur de retour. En tant qu'utilisateurs, nous sommes intéressés par la signature, et nous n'avons généralement pas besoin de connaître l'implémentation interne.

Imaginons maintenant que la signature de la fonction ressemble à ceci :

function addition(float $x): float

Une addition avec un seul paramètre ? C'est étrange… Qu'en est-il de ceci ?

function addition(): float

C'est vraiment bizarre, non ? Comment la fonction est-elle utilisée ?

echo addition(); // qu'est-ce que ça imprime ?

En regardant un tel code, nous serions confus. Non seulement un débutant ne le comprendrait pas, mais même un programmeur expérimenté ne le comprendrait pas.

Vous demandez-vous à quoi ressemblerait une telle fonction à l'intérieur ? Où obtiendrait-elle les sommets ? Elle les obtiendrait probablement d'elle-même, d'une manière ou d'une autre, par exemple de la manière suivante :

function addition(): float
{
	$a = Input::get('a');
	$b = Input::get('b');
	return $a + $b;
}

Il s'avère qu'il y a des liens cachés vers d'autres fonctions (ou méthodes statiques) dans le corps de la fonction, et pour trouver d'où viennent réellement les addends, nous devons creuser davantage.

Pas par là !

La conception que nous venons de montrer est l'essence même de nombreuses caractéristiques négatives :

  • la signature de la fonction prétend qu'elle n'a pas besoin des sommets, ce qui nous rend perplexes
  • nous n'avons aucune idée de la manière dont la fonction pourrait être calculée avec deux autres nombres
  • nous avons dû consulter le code pour savoir d'où venaient les sommations
  • nous avons découvert des dépendances cachées
  • pour bien comprendre, il faut aussi examiner ces dépendances

Et est-ce même le rôle de la fonction d'addition de se procurer des entrées ? Bien sûr que non. Sa responsabilité est uniquement d'ajouter.

Nous ne voulons pas rencontrer un tel code, et nous ne voulons certainement pas l'écrire. Le remède est simple : revenez à l'essentiel et utilisez simplement des paramètres :

function addition(float $a, float $b): float
{
	return $a + $b;
}

Règle n° 1 : Laissez-le vous être transmis

La règle la plus importante est la suivante : toutes les données dont les fonctions ou les classes ont besoin doivent leur être transmises.

Au lieu d'inventer des moyens cachés pour qu'ils accèdent eux-mêmes aux données, passez simplement les paramètres. Vous gagnerez du temps que vous auriez passé à inventer des chemins cachés qui n'amélioreront certainement pas votre code.

Si vous suivez toujours et partout cette règle, vous êtes sur la voie d'un code sans dépendances cachées. Un code compréhensible non seulement pour l'auteur mais aussi pour tous ceux qui le liront par la suite. Où tout est compréhensible à partir des signatures des fonctions et des classes, et où il n'est pas nécessaire de chercher des secrets cachés dans l'implémentation.

Cette technique est appelée professionnellement injection de dépendance. Et ces données sont appelées dépendances. Il s'agit d'un simple passage de paramètres, rien de plus.

Ne confondez pas l'injection de dépendances, qui est un modèle de conception, avec un “conteneur d'injection de dépendances”, qui est un outil, quelque chose de diamétralement différent. Nous traiterons des conteneurs plus tard.

Des fonctions aux classes

Et quel est le lien entre les classes ? Une classe est une unité plus complexe qu'une simple fonction, mais la règle n° 1 s'applique entièrement ici aussi. Il y a simplement plus de façons de passer des arguments. Par exemple, comme dans le cas d'une fonction :

class Math
{
	public function addition(float $a, float $b): float
	{
		return $a + $b;
	}
}

$math = new Math;
echo $math->addition(23, 1); // 24

Ou par d'autres méthodes, ou directement par le constructeur :

class Addition
{
	public function __construct(
		private float $a,
		private float $b,
	) {
	}

	public function calculate(): float
	{
		return $this->a + $this->b;
	}

}

$addition = new Addition(23, 1);
echo $addition->calculate(); // 24

Ces deux exemples sont tout à fait conformes à l'injection de dépendances.

Exemples concrets

Dans le monde réel, vous n'écrirez pas de cours sur l'addition de nombres. Passons maintenant aux exemples pratiques.

Prenons une classe Article représentant un article de blog :

class Article
{
	public int $id;
	public string $title;
	public string $content;

	public function save(): void
	{
		// sauvegarder l'article dans la base de données
	}
}

et l'utilisation sera la suivante :

$article = new Article;
$article->title = '10 Things You Need to Know About Losing Weight';
$article->content = 'Every year millions of people in ...';
$article->save();

La méthode save() enregistre l'article dans une table de la base de données. L'implémentation de cette méthode à l'aide de Nette Database serait un jeu d'enfant, si ce n'était d'un problème : où Article obtient-il la connexion à la base de données, c'est-à-dire un objet de la classe Nette\Database\Connection?

Il semble que nous ayons beaucoup d'options. Il peut l'obtenir à partir d'une variable statique quelque part. Ou hériter d'une classe qui fournit une connexion à la base de données. Ou tirer parti d'un singleton. Ou utiliser ce que l'on appelle les façades, qui sont utilisées dans Laravel :

use Illuminate\Support\Facades\DB;

class Article
{
	public int $id;
	public string $title;
	public string $content;

	public function save(): void
	{
		DB::insert(
			'INSERT INTO articles (title, content) VALUES (?, ?)',
			[$this->title, $this->content],
		);
	}
}

Super, nous avons résolu le problème.

Ou l'avons-nous fait ?

Rappelons la règle n°1 : Let It Be Passed to You: toutes les dépendances dont la classe a besoin doivent lui être transmises. Car si nous enfreignons cette règle, nous nous engageons sur la voie d'un code sale, plein de dépendances cachées, incompréhensible, et le résultat sera une application pénible à maintenir et à développer.

L'utilisateur de la classe Article n'a aucune idée de l'endroit où la méthode save() stocke l'article. Dans une table de la base de données ? Laquelle, celle de production ou celle de test ? Et comment la modifier ?

L'utilisateur doit regarder comment la méthode save() est implémentée et trouve l'utilisation de la méthode DB::insert(). Il doit donc poursuivre ses recherches pour découvrir comment cette méthode obtient une connexion à la base de données. Les dépendances cachées peuvent former une longue chaîne.

Dans un code propre et bien conçu, il n'y a jamais de dépendances cachées, de façades Laravel ou de variables statiques. Dans un code propre et bien conçu, les arguments sont transmis :

class Article
{
	public function save(Nette\Database\Connection $db): void
	{
		$db->query('INSERT INTO articles', [
			'title' => $this->title,
			'content' => $this->content,
		]);
	}
}

Une approche encore plus pratique, comme nous le verrons plus loin, consistera à utiliser le constructeur :

class Article
{
	public function __construct(
		private Nette\Database\Connection $db,
	) {
	}

	public function save(): void
	{
		$this->db->query('INSERT INTO articles', [
			'title' => $this->title,
			'content' => $this->content,
		]);
	}
}

Si vous êtes un programmeur expérimenté, vous pouvez penser que Article ne devrait pas avoir de méthode save() du tout ; il devrait représenter un composant purement de données, et un référentiel séparé devrait s'occuper de la sauvegarde. C'est logique. Mais cela nous mènerait bien au-delà du sujet, qui est l'injection de dépendances, et de l'effort pour fournir des exemples simples.

Si vous écrivez une classe qui a besoin, par exemple, d'une base de données pour fonctionner, n'inventez pas où aller la chercher, mais faites-la passer. Soit en tant que paramètre du constructeur, soit en tant que paramètre d'une autre méthode. Admettez les dépendances. Admettez-les dans l'API de votre classe. Vous obtiendrez un code compréhensible et prévisible.

Et que dire de cette classe qui enregistre les messages d'erreur :

class Logger
{
	public function log(string $message)
	{
		$file = LOG_DIR . '/log.txt';
		file_put_contents($file, $message . "\n", FILE_APPEND);
	}
}

Qu'en pensez-vous, avons-nous respecté la règle n°1 : laissez-le vous être transmis?

On ne l'a pas fait.

L'information clé, c'est-à-dire le répertoire contenant le fichier journal, est obtenue par la classe elle-même à partir de la constante.

Regardez l'exemple d'utilisation :

$logger = new Logger;
$logger->log('The temperature is 23 °C');
$logger->log('The temperature is 10 °C');

Sans connaître la mise en œuvre, pourriez-vous répondre à la question de savoir où les messages sont écrits ? Devinez-vous que l'existence de la constante LOG_DIR est nécessaire à son fonctionnement ? Et pourriez-vous créer une deuxième instance qui écrirait à un autre endroit ? Certainement pas.

Réparons la classe :

class Logger
{
	public function __construct(
		private string $file,
	) {
	}

	public function log(string $message): void
	{
		file_put_contents($this->file, $message . "\n", FILE_APPEND);
	}
}

La classe est désormais beaucoup plus compréhensible, configurable et donc plus utile.

$logger = new Logger('/path/to/log.txt');
$logger->log('The temperature is 15 °C');

Mais je m'en fiche !

“Lorsque je crée un objet Article et que j'appelle save(), je ne veux pas m'occuper de la base de données ; je veux juste qu'il soit enregistré dans celle que j'ai définie dans la configuration.”

“Lorsque j'utilise Logger, je veux juste que le message soit écrit, et je ne veux pas m'occuper de l'endroit. Laissez les paramètres globaux être utilisés.”

Ces points sont valables.

Prenons l'exemple d'une classe qui envoie des lettres d'information et enregistre leur déroulement :

class NewsletterDistributor
{
	public function distribute(): void
	{
		$logger = new Logger(/* ... */);
		try {
			$this->sendEmails();
			$logger->log('Emails have been sent out');

		} catch (Exception $e) {
			$logger->log('An error occurred during the sending');
			throw $e;
		}
	}
}

La version améliorée de Logger, qui n'utilise plus la constante LOG_DIR, nécessite de spécifier le chemin d'accès au fichier dans le constructeur. Comment résoudre ce problème ? La classe NewsletterDistributor ne se préoccupe pas de l'endroit où les messages sont écrits ; elle veut simplement les écrire.

La solution est encore une fois la règle n° 1 : “Let It Be Passed to You” : transmettez toutes les données dont la classe a besoin.

Cela signifie-t-il que nous transmettons le chemin d'accès au journal par l'intermédiaire du constructeur, que nous utilisons ensuite lors de la création de l'objet Logger?

class NewsletterDistributor
{
	public function __construct(
		private string $file, // ⛔ PAS DE CETTE FAÇON !
	) {
	}

	public function distribute(): void
	{
		$logger = new Logger($this->file);

Non, pas comme ça ! Le chemin d'accès ne fait pas partie des données dont la classe NewsletterDistributor a besoin ; en fait, c'est la classe Logger qui en a besoin. Voyez-vous la différence ? La classe NewsletterDistributor a besoin du logger lui-même. C'est donc ce que nous allons passer :

class NewsletterDistributor
{
	public function __construct(
		private Logger $logger, // ✅
	) {
	}

	public function distribute(): void
	{
		try {
			$this->sendEmails();
			$this->logger->log('Emails have been sent out');

		} catch (Exception $e) {
			$this->logger->log('An error occurred during the sending');
			throw $e;
		}
	}
}

Les signatures de la classe NewsletterDistributor montrent clairement que la journalisation fait également partie de ses fonctionnalités. Et la tâche consistant à remplacer le logger par un autre, par exemple pour des tests, est tout à fait triviale. De plus, si le constructeur de la classe Logger change, cela n'affectera pas notre classe.

Règle n° 2 : Prendre ce qui vous appartient

Ne vous laissez pas induire en erreur et ne vous laissez pas passer les dépendances de vos dépendances. Contentez-vous de passer vos propres dépendances.

Grâce à cela, le code utilisant d'autres objets sera complètement indépendant des changements dans leurs constructeurs. Son API sera plus véridique. Et surtout, il sera trivial de remplacer ces dépendances par d'autres.

Nouveau membre de la famille

L'équipe de développement a décidé de créer un deuxième enregistreur qui écrit dans la base de données. Nous avons donc créé une classe DatabaseLogger. Nous avons donc deux classes, Logger et DatabaseLogger, l'une écrit dans un fichier, l'autre dans une base de données … le nommage ne vous semble pas étrange ? Ne serait-il pas préférable de renommer Logger en FileLogger? Tout à fait.

Mais faisons-le intelligemment. Nous créons une interface sous le nom original :

interface Logger
{
	function log(string $message): void;
}

… que les deux bûcherons mettront en œuvre :

class FileLogger implements Logger
// ...

class DatabaseLogger implements Logger
// ...

De ce fait, il ne sera pas nécessaire de changer quoi que ce soit dans le reste du code où le logger est utilisé. Par exemple, le constructeur de la classe NewsletterDistributor se contentera toujours d'exiger Logger comme paramètre. Et il nous appartiendra de choisir l'instance que nous lui transmettrons.

C'est pourquoi nous n'ajoutons jamais le suffixe Interface ou le préfixe I aux noms d'interface, sans quoi il ne serait pas possible de développer le code de manière aussi agréable.

Houston, nous avons un problème

Alors que nous pouvons nous contenter d'une seule instance de l'enregistreur, qu'il soit basé sur un fichier ou une base de données, dans l'ensemble de l'application et qu'il suffit de le passer à chaque fois que quelque chose est enregistré, il en va tout autrement pour la classe Article. Nous créons ses instances en fonction des besoins, même plusieurs fois. Comment gérer la dépendance de la base de données dans son constructeur ?

Un exemple peut être un contrôleur qui doit enregistrer un article dans la base de données après avoir soumis un formulaire :

class EditController extends Controller
{
	public function formSubmitted($data)
	{
		$article = new Article(/* ... */);
		$article->title = $data->title;
		$article->content = $data->content;
		$article->save();
	}
}

Une solution possible est évidente : passer l'objet base de données au constructeur EditController et utiliser $article = new Article($this->db).

Tout comme dans le cas précédent avec Logger et le chemin d'accès au fichier, ce n'est pas la bonne approche. La base de données n'est pas une dépendance de EditController, mais de Article. Passer la base de données va à l'encontre de la règle #2 : prenez ce qui vous appartient. Si le constructeur de la classe Article change (un nouveau paramètre est ajouté), vous devrez modifier le code partout où des instances sont créées. Ufff.

Houston, que suggérez-vous ?

Règle n° 3 : Laissez l'usine s'en occuper

En éliminant les dépendances cachées et en passant toutes les dépendances en tant qu'arguments, nous avons obtenu des classes plus configurables et plus flexibles. Par conséquent, nous avons besoin de quelque chose d'autre pour créer et configurer ces classes plus flexibles pour nous. Nous l'appellerons “usine”.

La règle de base est la suivante : si une classe a des dépendances, laissez la création de leurs instances à la fabrique.

Les usines sont un remplacement plus intelligent de l'opérateur new dans le monde de l'injection de dépendances.

Ne pas confondre avec le modèle de conception factory method, qui décrit une manière spécifique d'utiliser les usines et n'est pas lié à ce sujet.

Usine

Une fabrique est une méthode ou une classe qui crée et configure des objets. Nous nommerons la classe produisant Article ArticleFactory , et elle pourrait ressembler à ceci :

class ArticleFactory
{
	public function __construct(
		private Nette\Database\Connection $db,
	) {
	}

	public function create(): Article
	{
		return new Article($this->db);
	}
}

Son utilisation dans le contrôleur sera la suivante :

class EditController extends Controller
{
	public function __construct(
		private ArticleFactory $articleFactory,
	) {
	}

	public function formSubmitted($data)
	{
		// laisser l'usine créer un objet
		$article = $this->articleFactory->create();
		$article->title = $data->title;
		$article->content = $data->content;
		$article->save();
	}
}

À ce stade, si la signature du constructeur de la classe Article change, la seule partie du code qui doit réagir est le ArticleFactory lui-même. Tout autre code travaillant avec des objets Article, comme EditController, ne sera pas affecté.

Vous vous demandez peut-être si nous avons vraiment amélioré les choses. La quantité de code a augmenté et tout cela commence à sembler étrangement compliqué.

Ne vous inquiétez pas, nous allons bientôt aborder le conteneur Nette DI. Il a plus d'un tour dans son sac, ce qui simplifiera grandement la construction d'applications utilisant l'injection de dépendances. Par exemple, au lieu de la classe ArticleFactory, vous n'aurez qu'à écrire une simple interface:

interface ArticleFactory
{
	function create(): Article;
}

Mais nous prenons de l'avance ; soyez patients :-)

Résumé

Au début de ce chapitre, nous avons promis de vous montrer un processus de conception de code propre. Tout ce qu'il faut, c'est que les classes.. :

À première vue, ces trois règles ne semblent pas avoir de conséquences importantes, mais elles conduisent à une perspective radicalement différente de la conception du code. Le jeu en vaut-il la chandelle ? Les développeurs qui ont abandonné leurs vieilles habitudes et commencé à utiliser systématiquement l'injection de dépendances considèrent cette étape comme un moment crucial de leur vie professionnelle. Elle leur a ouvert le monde des applications claires et faciles à maintenir.

Mais que se passe-t-il si le code n'utilise pas systématiquement l'injection de dépendances ? Que se passe-t-il s'il s'appuie sur des méthodes statiques ou des singletons ? Cela pose-t-il des problèmes ? Oui, cela pose des problèmes, et des problèmes fondamentaux.

version: 3.x