Le Dependency Inversion Principle (DIP) est le cinquième et dernier des principes SOLID. Il est souvent perçu comme l’un des plus puissants mais c’est aussi probablement le moins bien compris. Ce principe est fondamental pour créer une architecture flexible, testable et maintenable.
Définition
Les modules de haut niveau ne doivent pas dépendre de modules de bas niveau. Tous deux doivent dépendre d’abstractions.
Les abstractions ne doivent pas dépendre des détails. Ce sont les détails qui doivent dépendre des abstractions.
Autrement dit : ne liez pas vos classes à des implémentations concrètes, mais à des comportements abstraits (généralement des interfaces).
Une analogie visuelle : le robot et l’armoire à bras
Imaginez un robot dans un atelier high-tech. Il possède une interface standard sur son épaule pour connecter un bras. Ce robot peut recevoir n’importe quel bras parmi ceux disponibles dans une armoire : un bras laser, un bras à lame, un bras de précision, un bras avec perceuse…
Le robot ne connaît pas les détails internes de chaque bras. Il sait seulement comment activer ou désactiver un bras via une interface commune. Ce sont les bras qui s’adaptent à cette interface standard, pas le robot qui doit changer sa logique à chaque nouveau type d’outil.
C’est exactement le principe de l’inversion de dépendance : le module de haut niveau (le robot) dépend d’une abstraction (l’interface de connexion), et ce sont les modules de bas niveau (les bras-outils) qui s’y adaptent.
Qu’est-ce qu’une dépendance ?
Une dépendance représente tout objet dont une classe a besoin pour fonctionner. Par exemple, une classe de service peut dépendre d’un autre objet comme un repository, un utilitaire ou encore un service tiers qu’elle va appeler pour accomplir sa tâche.
En Java, on reconnaît une dépendance lorsqu’une classe utilise une autre classe pour fonctionner :
Voici un premier (très mauvais) exemple où la dépendance est directement instanciée à l’intérieur de la classe :
class ServiceA {
private final ServiceB b = new ServiceB();
}
Ici, ServiceA
est étroitement couplé à ServiceB
car elle le crée elle-même. Si l’implémentation de ServiceB
change, ServiceA
devra être modifiée également. Ce couplage fort viole le principe d’inversion de dépendance.
Une amélioration consiste à injecter ServiceB
depuis l’extérieur (on appelle ça l’injection de dépendance) :
class ServiceA {
private final ServiceB b;
public ServiceA(ServiceB b) {
this.b = b;
}
}
Ici, ServiceA
dépend toujours de ServiceB
, même si on ne l’instancie plus à l’intérieur. Ce point est essentiel à bien comprendre : l’injection de dépendance (DI) désigne simplement le fait de passer une dépendance depuis l’extérieur, plutôt que de l’instancier dans la classe. Mais cela ne signifie pas forcément que la dépendance est abstraite. On peut très bien injecter une dépendance concrète, et dans ce cas, le DIP n’est pas respecté.
Le DIP va plus loin : il impose que la dépendance injectée soit une abstraction (généralement une interface), ce qui permet de découpler complètement les modules entre eux. Ainsi, le module de haut niveau reste indépendant des détails d’implémentation, qui peuvent varier, être remplacés ou simulés en test sans modifier le code métier.
Pour appliquer le DIP, il faudrait que ServiceA
ne dépende pas directement de ServiceB
, mais d’une abstraction (une interface ou une classe abstraite) que ServiceB
implémenterait. Ce n’est donc pas la manière dont la dépendance est injectée qui compte, mais de quoi la classe dépend.
interface ServiceBInterface {
void executer();
}
class ServiceBImpl implements ServiceBInterface {
public void executer() {
System.out.println("Service B exécuté");
}
}
class ServiceA {
private final ServiceBInterface b;
public ServiceA(ServiceBInterface b) {
this.b = b;
}
public void faireQuelqueChose() {
b.executer();
}
}
ServiceA
dépend maintenant de l’interfaceServiceBInterface
.- N’importe quelle implémentation (réelle, mock, alternative) peut être injectée.
- On respecte le DIP car le module de haut niveau dépend d’une abstraction. (une interface ou une classe abstraite) que
ServiceB
implémenterait.
Quels sont les problèmes d’une dépendance directe ?
1. Couplage fort
Votre classe est liée à une implémentation spécifique. Si ServiceB
change, ServiceA
devra probablement être modifiée aussi.
2. Difficulté à tester
Il est impossible de remplacer facilement ServiceB
par un mock ou une implémentation de test.
3. Manque de flexibilité
Impossible de substituer dynamiquement un autre comportement sans modifier le code source.
4. Propagation des changements
Un changement dans une classe bas niveau peut se répercuter sur toutes les classes qui en dépendent directement.
Mauvais exemple : dépendance concrète
class NotificationService {
public void envoyer(String message) {
System.out.println("Notification envoyée : " + message);
}
}
class CommandeService {
private final NotificationService notificationService;
public CommandeService(NotificationService notificationService) {
this.notificationService = notificationService;
}
public void traiterCommande() {
System.out.println("Commande traitée.");
notificationService.envoyer("Confirmation envoyée.");
}
}
CommandeService
est verrouillé à l’implémentation concrèteNotificationService
.- Impossible de tester
CommandeService
sans réellement envoyer des notifications.
Bonnes pratiques : appliquer le DIP
Étape 1 : créer une abstraction
interface Notificateur {
void envoyer(String message);
}
Étape 2 : faire dépendre le module de haut niveau de cette abstraction
class CommandeService {
private final Notificateur notificateur;
public CommandeService(Notificateur notificateur) {
this.notificateur = notificateur;
}
public void traiterCommande() {
System.out.println("Commande traitée.");
notificateur.envoyer("Confirmation envoyée.");
}
}
Étape 3 : fournir une implémentation concrète au moment de l’exécution
class NotificationEmail implements Notificateur {
public void envoyer(String message) {
System.out.println("Email : " + message);
}
}
class Main {
public static void main(String[] args) {
Notificateur email = new NotificationEmail();
CommandeService service = new CommandeService(email);
service.traiterCommande();
}
}
✅ CommandeService
ne dépend plus de NotificationEmail
, mais de Notificateur
.
➡️ Il est donc facile de remplacer l’implémentation par NotificationSMS
, NotificationSlack
, ou un mock en test.
En résumé
Le principe d’inversion des dépendances vous encourage à :
- dépendre d’abstractions plutôt que d’implémentations concrètes
- inverser le contrôle : ce n’est plus la classe qui instancie ses dépendances, mais l’extérieur qui les injecte
- rendre votre code plus flexible, testable, évolutif
Respecter le DIP, c’est faire un pas vers une architecture propre, modulaire, et durable.
À retenir
- Ne créez pas vos dépendances dans vos classes : injectez-les.
- Définissez des interfaces pour les comportements.
- Faites dépendre vos classes d’abstractions.