Les principes SOLID en Java
Appliquez les principes SOLID — SRP, OCP, LSP, ISP, DIP — à la conception Java.
SOLID est un ensemble de cinq principes de conception orientée objet — popularisés par Robert C. Martin — qui permettent de garder le code Java facile à modifier, tester et étendre au fur et à mesure de sa croissance. Ce ne sont pas des règles syntaxiques que le compilateur impose ; ce sont des lignes directrices pour définir les frontières entre les classes afin qu'un changement ne se propage pas à tout le code. L'acronyme signifie Single Responsibility (responsabilité unique), Open/Closed (ouvert/fermé), Liskov Substitution (substitution de Liskov), Interface Segregation (ségrégation des interfaces) et Dependency Inversion (inversion des dépendances).
Ces principes s'appuient sur les bases orientées objet : vous verrez des interfaces, de l'héritage et du polymorphisme tout au long du chapitre. Si ces notions vous semblent floues, revoyez-les d'abord — SOLID consiste essentiellement à savoir où les appliquer judicieusement.
Les cinq principes en un coup d'œil
Chaque lettre cible un type spécifique de problème de conception. Gardez ce tableau à portée pendant la lecture du chapitre :
| Lettre | Principe | Objectif en une ligne |
|---|---|---|
| S | Single Responsibility | Une classe doit avoir une seule raison de changer |
| O | Open/Closed | Ouverte à l'extension, fermée à la modification |
| L | Liskov Substitution | Les sous-types doivent être utilisables là où leur type de base l'est |
| I | Interface Segregation | Plusieurs petites interfaces valent mieux qu'une grosse |
| D | Dependency Inversion | Dépendre des abstractions, pas des classes concrètes |
Les principes se renforcent mutuellement. Dans un code bien structuré, on en applique rarement qu'un seul — une petite interface (ISP) dont dépend le code de haut niveau (DIP) est exactement ce qui permet d'ajouter une nouvelle implémentation (OCP) sans toucher à l'appelant.
S — Principe de responsabilité unique
Une classe doit faire une seule chose et avoir une seule raison de changer. Quand des préoccupations sans lien — règles métier et envoi de messages, par exemple — partagent une même classe, un changement de l'une force à retester les deux. Les séparer isole les changements.
// Mixes WHEN to alert with HOW to deliver -- two reasons to change.
class BadAlertService {
void raise(String user, int errors) {
if (errors > 0) {
// ...build an email, open an SMTP connection, send...
}
}
}
// One responsibility: deciding when to alert. Delivery lives elsewhere.
class AlertService {
private final Notifier notifier;
AlertService(Notifier notifier) { this.notifier = notifier; }
void raise(String user, int errors) {
if (errors > 0) notifier.send(user, errors + " error(s) detected");
}
}O — Principe ouvert/fermé
Les entités logicielles doivent être ouvertes à l'extension mais fermées à la modification. Vous devez pouvoir ajouter un nouveau comportement en écrivant du nouveau code, et non en modifiant — et en risquant de casser — du code qui fonctionne déjà. En Java, le levier habituel est une interface stable associée à de nouvelles implémentations.
interface Notifier { void send(String to, String message); }
class EmailNotifier implements Notifier { /* ... */ }
class SmsNotifier implements Notifier { /* ... */ } // new feature = new class
// AlertService never changes when a new channel appears.Ajouter les notifications push plus tard signifie écrire PushNotifier implements Notifier — AlertService reste intact, sans besoin de révision ni risque de régression.
L — Principe de substitution de Liskov
Si S est un sous-type de T, alors les objets de type T peuvent être remplacés par des objets de type S sans casser le programme. Une sous-classe doit respecter le contrat de sa classe parente — mêmes attentes, pas d'exceptions surprenantes, pas de préconditions plus strictes.
abstract class Shape { abstract double area(); }
class Rectangle extends Shape { /* area() = w * h */ }
class Circle extends Shape { /* area() = PI * r * r */ }
// Works for ANY Shape, present or future, without inspecting the concrete type.
double totalArea(List<Shape> shapes) {
return shapes.stream().mapToDouble(Shape::area).sum();
}La violation classique est Square extends Rectangle : si définir la largeur modifie aussi la hauteur, le code écrit pour un Rectangle plante quand on lui passe un Square. La solution consiste à les modéliser comme des siblings sous Shape, et non comme une paire parent-enfant. (Voir les classes abstraites pour la base Shape utilisée ici.)
I — Principe de ségrégation des interfaces
Les clients ne devraient pas être forcés de dépendre de méthodes qu'ils n'utilisent pas. Préférez plusieurs interfaces petites et ciblées à une grande — sinon un implémenteur se retrouve à écrire des méthodes vides qu'il ne peut pas honorer.
// Fat interface: a read-only source is forced to implement write().
interface Storage { String read(); void write(String data); }
// Segregated: implement only what you can honor.
interface Readable { String read(); }
interface Writable { void write(String data); }
class ConfigFile implements Readable { // no empty write() stub
public String read() { return "mode=prod"; }
}D — Principe d'inversion des dépendances
Les modules de haut niveau ne doivent pas dépendre des modules de bas niveau ; les deux doivent dépendre des abstractions. En pratique : programmez contre des interfaces et injectez l'implémentation concrète (l'injection par constructeur est la forme la plus simple). C'est ce qui rend les autres principes payants — et ce qui rend une classe testable, puisqu'on peut lui passer un faux objet.
// AlertService depends on the Notifier interface, not EmailNotifier.
AlertService alerts = new AlertService(new EmailNotifier());
// In a test, inject a fake Notifier and assert on what it recorded.Un exemple concret : les cinq principes dans un seul programme
Ce programme assemble les principes — un seul AlertService (SRP) communique avec un Notifier injecté (DIP), bascule entre un EmailNotifier et un SmsNotifier sans changer (OCP), lit un ConfigFile uniquement Readable (ISP), et calcule les surfaces de sous-types de Shape de manière uniforme (LSP). Il vérifie ses propres résultats pour que vous puissiez constater chaque principe en action.
Ce qu'il faut retenir de l'exécution :
email sent: [EMAIL -> alice: 3 error(s) detected]ne contient qu'une entrée —bobn'avait aucune erreur, doncraisen'a rien envoyé.AlertServiceporte la responsabilité unique de décider quand alerter (SRP) ; il ne construit jamais le corps du message ni n'ouvre de connexion.- La même classe
AlertServicea géré à la fois unEmailNotifieret unSmsNotifierparce que la dépendance lui a été fournie via son constructeur (DIP). La logique d'alerte de haut niveau ne dépend que de l'interfaceNotifier, jamais d'un expéditeur concret. OCP check : ... unchanged = trueconfirme que les deux objets d'alerte sont la même classeAlertService: ajouter le support SMS a consisté à écrire un nouveauSmsNotifier, sans aucune modification d'AlertService— ouvert à l'extension, fermé à la modification.ISP check : is Writable? falsemontre queConfigFileimplémente uniquementReadable. Grâce à la ségrégation des interfaces, la source en lecture seule n'a jamais été forcée de fournir un stubwritesans signification.LSP area : 9.142est la somme d'un rectangle 2×3 (6,0) et d'un cercle de rayon 1 (≈3,142).totalAreaa parcouru des référencesShapeet appeléarea()sans vérifier quel sous-type il détenait — chaque sous-type était substituable à sa base (LSP).