Le Pattern Dependency Injection expliqué simplement
L’injection de dépendances (Dependency Injection) consiste à fournir à un objet ce dont il a besoin depuis l’extérieur, plutôt que de le laisser construire lui-même ses dépendances. Un principe simple qui rend le code testable et remplaçable.
Sommaire
Le problème #
Une classe ServiceInscription a besoin d’envoyer un e-mail de bienvenue. La solution la plus directe : créer l’instance du mailer directement à l’intérieur de la classe, avec new SmtpMailer(). Ça fonctionne, mais ce choix a un coût caché.
ServiceInscription dépend maintenant d’une implémentation SMTP précise, câblée en dur. Impossible de tester la classe sans envoyer un vrai e-mail à chaque test. Impossible de changer de fournisseur d’e-mail (passer à un service tiers, ou à un mailer factice en environnement de développement) sans modifier le code de ServiceInscription lui-même. La classe qui devrait se concentrer sur la logique d’inscription se retrouve responsable, en plus, de savoir comment construire un mailer SMTP.
L’idée générale #
L’injection de dépendances renverse la responsabilité de construction : au lieu qu’une classe construise elle-même ce dont elle a besoin, elle le reçoit de l’extérieur, déjà prêt à l’emploi. La classe dépend d’une interface, jamais d’une implémentation concrète qu’elle devrait instancier.
Trois formes principales existent :
- Injection par constructeur : les dépendances sont passées au moment de la création de l’objet. C’est la forme à privilégier pour les dépendances obligatoires — l’objet n’existe jamais dans un état incomplet.
- Injection par setter : les dépendances sont fournies après la construction, via une méthode dédiée. Utile pour des dépendances optionnelles ou remplaçables en cours de vie de l’objet.
- Injection par interface : la classe implémente une interface qui impose une méthode d’injection, moins courante en pratique.
Un conteneur DI (comme le composant Symfony DependencyInjection, Spring en Java, ou InversifyJS/NestJS en JavaScript) automatise ce câblage : il sait construire l’arbre complet des dépendances et les injecter au bon endroit. Le conteneur est un outil pratique, pas le principe lui-même — on peut faire de l’injection de dépendances à la main, sans aucun conteneur, en câblant tout au point d’entrée de l’application.
Analogie du quotidien #
L’injection de dépendances, c’est comme une lampe de chevet branchée sur le secteur. La lampe a besoin d’électricité pour fonctionner, mais elle ne la produit pas elle-même : elle se contente d’un fil et d’une prise standard.
Peu importe que le courant vienne d’une centrale nucléaire, d’un panneau solaire ou d’un groupe électrogène — la lampe fonctionne exactement pareil, sans rien savoir de la source. Si vous changez de fournisseur d’électricité, vous ne démontez pas la lampe : vous changez ce qui est branché derrière la prise. La lampe (la classe) dépend d’une interface standard (la prise), jamais d’une centrale précise (une implémentation concrète).
Diagramme #
Exemple de code #
PHP #
interface MailerInterface
{
public function envoyer(string $destinataire, string $message): void;
}
final class SmtpMailer implements MailerInterface
{
public function envoyer(string $destinataire, string $message): void
{
// Envoi réel via SMTP...
mail($destinataire, 'Bienvenue', $message);
}
}
// --- Injection par constructeur : dépendance obligatoire ---
final class ServiceInscription
{
public function __construct(private MailerInterface $mailer) {}
public function inscrire(string $email): void
{
// ... logique d'inscription ...
$this->mailer->envoyer($email, 'Bienvenue parmi nous !');
}
}
// Câblage manuel, sans conteneur
$service = new ServiceInscription(new SmtpMailer());
// Avec un conteneur DI (exemple Symfony), le câblage est automatique :
// $container->set(MailerInterface::class, SmtpMailer::class);
// $service = $container->get(ServiceInscription::class);
// En test, on injecte un double sans jamais toucher SmtpMailer
final class MailerDeTest implements MailerInterface
{
public array $messagesEnvoyes = [];
public function envoyer(string $destinataire, string $message): void
{
$this->messagesEnvoyes[] = [$destinataire, $message];
}
}
Java #
public interface Mailer {
void envoyer(String destinataire, String message);
}
public class SmtpMailer implements Mailer {
public void envoyer(String destinataire, String message) {
// Envoi réel via SMTP...
System.out.println("Email envoyé à " + destinataire);
}
}
// --- Injection par constructeur : dépendance obligatoire ---
public class ServiceInscription {
private final Mailer mailer;
public ServiceInscription(Mailer mailer) {
this.mailer = mailer;
}
public void inscrire(String email) {
// ... logique d'inscription ...
mailer.envoyer(email, "Bienvenue parmi nous !");
}
}
// Câblage manuel
ServiceInscription service = new ServiceInscription(new SmtpMailer());
// Avec Spring, le conteneur automatise l'injection :
// @Service
// public class ServiceInscription {
// public ServiceInscription(Mailer mailer) { this.mailer = mailer; } // @Autowired implicite
// }
// --- Injection par setter : dépendance optionnelle ou remplaçable ---
public class ServiceNotification {
private Mailer mailer;
public void setMailer(Mailer mailer) {
this.mailer = mailer;
}
}
JavaScript #
class SmtpMailer {
envoyer(destinataire, message) {
// Envoi réel via SMTP...
console.log(`Email envoyé à ${destinataire}`);
}
}
// --- Injection par constructeur : dépendance obligatoire ---
class ServiceInscription {
constructor(mailer) {
this.mailer = mailer;
}
inscrire(email) {
// ... logique d'inscription ...
this.mailer.envoyer(email, "Bienvenue parmi nous !");
}
}
// Câblage manuel, sans conteneur
const service = new ServiceInscription(new SmtpMailer());
// Avec un conteneur DI (ex. NestJS), le câblage est automatique via décorateurs :
// @Injectable()
// class ServiceInscription {
// constructor(private mailer) {}
// }
// En test, on injecte un double sans jamais toucher SmtpMailer
class MailerDeTest {
constructor() {
this.messagesEnvoyes = [];
}
envoyer(destinataire, message) {
this.messagesEnvoyes.push({ destinataire, message });
}
}
const serviceDeTest = new ServiceInscription(new MailerDeTest());
Quand utiliser ce pattern ? #
- Dès qu’une classe dépend d’un service dont l’implémentation peut varier entre les environnements (production, test, développement local) : mailer, logger, client HTTP, repository.
- Quand vous voulez tester une classe en isolation, sans dépendre d’une infrastructure réelle (base de données, SMTP, API externe).
- Quand le graphe de dépendances devient large : c’est là qu’un conteneur DI devient utile, pour éviter de câbler des dizaines d’objets à la main.
- En pratique, c’est une bonne pratique par défaut plutôt qu’un choix ponctuel : instancier une dépendance en dur avec
newà l’intérieur d’une classe doit rester l’exception, justifiée, pas la norme.
Points importants #
- Ne confondez pas le principe (injecter plutôt que construire) et l’outil (un conteneur DI) : le premier s’applique même dans un script de dix lignes, le second n’est utile qu’à partir d’un graphe de dépendances conséquent.
- L’injection par constructeur est la forme par défaut : elle rend les dépendances visibles dans la signature, et garantit qu’un objet mal construit ne peut pas exister.
- Le “point de composition” (l’endroit où toutes les dépendances sont assemblées) doit rester unique et proche du point d’entrée de l’application — jamais éparpillé dans la logique métier.
- Attention au Service Locator, souvent présenté comme une alternative pratique à DI : il masque les dépendances réelles d’une classe et rend le code plus difficile à tester et à comprendre à la simple lecture d’un constructeur.
🐻 À retenir
- ●DI consiste à recevoir ses dépendances (constructeur, setter) plutôt qu’à les construire soi-même avec
newà l’intérieur d’une classe. - ●L’injection par constructeur est la forme par défaut à privilégier : elle garantit qu’un objet est toujours dans un état valide, dépendances obligatoires incluses.
- ●Un conteneur DI (Symfony, Spring, etc.) n’est qu’un outil qui automatise le câblage : le principe DI fonctionne très bien sans conteneur, à la main, au point d’entrée de l’application.
Questions d'entretien
Quelle différence entre Dependency Injection et Service Locator ?
ServiceLocator::get(MailerInterface::class)) : les dépendances sont cachées à l’intérieur du code, invisibles depuis la signature du constructeur. Avec Dependency Injection, les dépendances sont explicites et révélées par le constructeur — on voit immédiatement de quoi une classe a besoin, et on peut les remplacer facilement par des mocks en test. C’est pour cette raison que Service Locator est aujourd’hui considéré comme un anti-pattern dans la plupart des contextes.