W3docs

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 :

LettrePrincipeObjectif en une ligne
SSingle ResponsibilityUne classe doit avoir une seule raison de changer
OOpen/ClosedOuverte à l'extension, fermée à la modification
LLiskov SubstitutionLes sous-types doivent être utilisables là où leur type de base l'est
IInterface SegregationPlusieurs petites interfaces valent mieux qu'une grosse
DDependency InversionDé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 NotifierAlertService 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.

java— editable, runs on the server

Ce qu'il faut retenir de l'exécution :

  • email sent: [EMAIL -> alice: 3 error(s) detected] ne contient qu'une entrée — bob n'avait aucune erreur, donc raise n'a rien envoyé. AlertService porte 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 AlertService a géré à la fois un EmailNotifier et un SmsNotifier parce 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'interface Notifier, jamais d'un expéditeur concret.
  • OCP check : ... unchanged = true confirme que les deux objets d'alerte sont la même classe AlertService : ajouter le support SMS a consisté à écrire un nouveau SmsNotifier, sans aucune modification d'AlertService — ouvert à l'extension, fermé à la modification.
  • ISP check : is Writable? false montre que ConfigFile implémente uniquement Readable. Grâce à la ségrégation des interfaces, la source en lecture seule n'a jamais été forcée de fournir un stub write sans signification.
  • LSP area : 9.142 est la somme d'un rectangle 2×3 (6,0) et d'un cercle de rayon 1 (≈3,142). totalArea a parcouru des références Shape et appelé area() sans vérifier quel sous-type il détenait — chaque sous-type était substituable à sa base (LSP).

Exercice

Pratique
Une classe nommée ReportGenerator formate à la fois les données du rapport et les écrit sur le disque, de sorte que tout changement des règles de formatage ou de la structure du fichier oblige à modifier et retester la même classe. Quel principe SOLID cela viole-t-il le plus directement ?
Une classe nommée ReportGenerator formate à la fois les données du rapport et les écrit sur le disque, de sorte que tout changement des règles de formatage ou de la structure du fichier oblige à modifier et retester la même classe. Quel principe SOLID cela viole-t-il le plus directement ?
Was this page helpful?