SOLID est l'acronyme de cinq principes de conception orientée objet introduits par Robert Martin au début des années 2000. Chaque principe répond à un mode de défaillance spécifique de la conception orientée objet – des symptômes que vous reconnaissez dans les bases de code réelles : des classes impossibles à étendre, des modules qui se brisent à des endroits inattendus lorsqu'ils sont modifiés, un code qui ne peut pas être testé de manière isolée. Les principes SOLID ne sont pas des règles à appliquer mécaniquement : ce sont des objectifs permettant d'identifier les problèmes de conception et de guider la refactorisation. Cette leçon couvre chaque principe avec précision, montre la violation avant le correctif et explique les implications architecturales.

Les principes SOLID sont des moyens et non des fins. Une classe qui viole SRP mais qui fait 50 lignes, n’a aucun collaborateur et ne changera jamais, c’est bien. Une classe qui suit chaque principe mécaniquement mais ajoute trois niveaux d'indirection à une opération triviale est pire que la violation. Appliquer des principes là où ils résolvent un problème réel.
S — Principe de responsabilité unique

La formulation de Martin : « Une classe ne devrait avoir qu'une seule raison de changer. » Le mot « raison » est essentiel : il fait référence à un acteur ou à une partie prenante dont les exigences pourraient conduire à un changement, et non à une règle syntaxique « à méthode unique ». Une classe qui formate les rapports ET les enregistre dans la base de données a deux raisons de changer : l'équipe de reporting (changements de format) et l'équipe opérationnelle (migration de la base de données). Lorsque ces changements se produisent simultanément, ils entrent en conflit. SRP affirme que ces préoccupations appartiennent à des classes distinctes.

Cliquez sur les lignes en surbrillance pour comprendre la violation SRP et le correctif
java
1
// ── VIOLATION: two reasons to change in one class ───────────────
2
class OrderProcessor {
3
  void process(Order order) {
4
    // business logic
5
    order.setStatus(CONFIRMED);
6
    // persistence — reason #2 to change
7
    db.save(order);
8
    // notification — reason #3 to change
9
    emailClient.send(order.getEmail(), buildTemplate(order));
10
  }
11
}
12
13
// ── FIX: each class has one reason to change ─────────────────────
14
class OrderConfirmationService {
15
  OrderConfirmationService(OrderRepository repo, OrderNotificationService notifier) { ... }
16
  void confirm(Order order) {
17
    order.confirm();          // domain logic only
18
    repo.save(order);         // delegates persistence
19
    notifier.sendConfirmation(order); // delegates notification
20
  }
21
}
22
class OrderRepository      { void save(Order o) { ... } }   // DB only
23
class OrderNotificationService { void sendConfirmation(Order o) { ... } } // email only
O — Principe ouvert/fermé

Formulation de Bertrand Meyer (1988), reformulée par Martin : « Les entités logicielles doivent être ouvertes à l'extension, mais fermées à la modification ». Une classe est fermée pour modification lorsque son code testé et déployé n'a pas besoin d'être modifié pour s'adapter à un nouveau comportement. Il est ouvert à l'extension lorsqu'un nouveau comportement peut être ajouté en écrivant un nouveau code — une nouvelle sous-classe, une nouvelle implémentation de stratégie, un nouveau décorateur — sans toucher à la classe existante. Le mécanisme le plus courant : dépendre d'abstractions (interfaces) et injecter des implémentations concrètes.

Cliquez sur les lignes en surbrillance pour voir OCP violé et corrigé
java
1
// ── VIOLATION: every new payment method requires modifying this class
2
class PaymentProcessor {
3
  void process(Payment p) {
4
    if (p.type() == CREDIT_CARD) chargeCard(p);
5
    else if (p.type() == PAYPAL)  chargePayPal(p);
6
    else if (p.type() == CRYPTO)  chargeCrypto(p);  // added later, modified existing class
7
  }
8
}
9
10
// ── FIX: closed for modification, open for extension ────────────
11
interface PaymentGateway { void charge(Payment p); }
12
class CreditCardGateway  implements PaymentGateway { ... }
13
class PayPalGateway       implements PaymentGateway { ... }
14
class CryptoGateway       implements PaymentGateway { ... }  // new method = new class only
15
16
class PaymentProcessor {
17
  private final Map<PaymentType, PaymentGateway> gateways;
18
  void process(Payment p) {
19
    gateways.get(p.type()).charge(p);  // never changes
20
  }
21
}
L — Principe de substitution de Liskov

Formulation de Barbara Liskov (1987) : « 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 altérer aucune des propriétés souhaitables du programme. » En termes simples : une sous-classe doit honorer le contrat de sa superclasse – pas seulement ses signatures de méthode, mais ses attentes comportementales (préconditions, postconditions, invariants). La violation LSP classique est le problème Carré-Rectangle : Carré étend le rectangle, mais remplacer setWidth pour définir également la hauteur viole le contrat du rectangle selon lequel la largeur et la hauteur sont indépendantes.

Cliquez sur les lignes en surbrillance pour comprendre la violation LSP et la conception correcte
java
1
// ── VIOLATION: Square breaks Rectangle's behavioral contract ────
2
class Rectangle {
3
  protected int width, height;
4
  void setWidth(int w)  { this.width = w; }
5
  void setHeight(int h) { this.height = h; }
6
  int area() { return width * height; }
7
}
8
class Square extends Rectangle {
9
  @Override void setWidth(int w)  { width = height = w; }  // breaks rectangle contract
10
  @Override void setHeight(int h) { width = height = h; }  // breaks rectangle contract
11
}
12
// Client code breaks when Square is substituted for Rectangle:
13
// rect.setWidth(5); rect.setHeight(4); assert rect.area() == 20; // FAILS for Square
14
15
// ── FIX: model the actual relationship — no inheritance ─────────
16
interface Shape { int area(); }
17
record Rectangle(int width, int height) implements Shape {
18
  public int area() { return width * height; }
19
}
20
record Square(int side) implements Shape {
21
  public int area() { return side * side; }
22
}
I — Principe de ségrégation des interfaces

Formulation de Martin : « Les clients ne devraient pas être obligés de dépendre d'interfaces qu'ils n'utilisent pas. » Les grosses interfaces - des interfaces avec de nombreuses méthodes indépendantes - obligent les implémenteurs à fournir des implémentations de stub de méthodes dont ils n'ont pas besoin et obligent les clients à importer un type qui comporte des fonctionnalités qu'ils n'utilisent pas. Le FAI dit : préférez de nombreuses petites interfaces spécifiques au client plutôt qu'une grande interface à usage général. La pression de conception exercée par les FAI produit naturellement des interfaces de rôle : des interfaces qui décrivent le rôle joué par un objet (imprimable, enregistrable, auditable) plutôt que le menu complet de tout ce qu'un objet peut faire.

Cliquez sur les lignes en surbrillance pour comprendre la violation du FAI et les interfaces de rôle
java
1
// ── VIOLATION: fat interface forces irrelevant method stubs ─────
2
interface Worker {
3
  void work();
4
  void takeBreak();   // irrelevant for Robot
5
  void receivePaycheck(); // irrelevant for Robot
6
}
7
class Robot implements Worker {
8
  public void work() { ... }
9
  public void takeBreak() { /* Robot does not rest — forced stub */ }
10
  public void receivePaycheck() { /* Robot is not paid — forced stub */ }
11
}
12
13
// ── FIX: small role interfaces ───────────────────────────────────
14
interface Workable  { void work(); }
15
interface Breakable { void takeBreak(); }
16
interface Payable   { void receivePaycheck(); }
17
18
class Robot      implements Workable { ... }                        // only what it needs
19
class HumanWorker implements Workable, Breakable, Payable { ... }   // full set
20
21
// Scheduler only needs Workable — does not know about breaks or pay:
22
class ProductionScheduler { void schedule(List<Workable> workers) { ... } }
D — Principe d'inversion de dépendance

Formulation de Martin : « (A) Les modules de haut niveau ne devraient pas dépendre des modules de bas niveau. Les deux devraient dépendre d’abstractions. (B) Les abstractions ne devraient pas dépendre des détails. Les détails devraient dépendre des abstractions. DIP est le principe qui sous-tend l'architecture Onion, l'architecture hexagonale et l'architecture propre – toutes les architectures abordées dans le chapitre précédent. C’est aussi le principe qui donne du sens à l’injection de dépendances : DI est le mécanisme ; DIP est la raison.

Cliquez sur les lignes en surbrillance pour retracer l'inversion de dépendance depuis la violation jusqu'à la correction
java
1
// ── VIOLATION: high-level module depends on low-level detail ────
2
class OrderService {
3
  private final MySqlOrderRepository repo = new MySqlOrderRepository();
4
  void confirm(Long id) {
5
    Order order = repo.findById(id);
6
    order.confirm();
7
    repo.save(order);
8
  }
9
}
10
11
// ── FIX: both depend on the abstraction ──────────────────────────
12
interface OrderRepository { Order findById(Long id); void save(Order o); }
13
14
class OrderService {
15
  private final OrderRepository repo;  // depends on abstraction
16
  OrderService(OrderRepository repo) { this.repo = repo; }  // DI via constructor
17
  void confirm(Long id) { ... }  // unchanged logic
18
}
19
20
// Low-level detail depends on abstraction (not vice versa):
21
class MySqlOrderRepository implements OrderRepository { ... }    // production
22
class InMemoryOrderRepository implements OrderRepository { ... } // tests
SOLID en pratique : vue d’ensemble
Référence rapide : problème que chaque principe résout et mécanisme de conception qu'il prescrit
PrincipeSymptôme qu'il corrigeMécanismeImpact architectural
PDSLes classes changent pour plusieurs raisons indépendantes ; fusionner les conflits ; tests dispersésRépartition par acteur/raison du changementEntraîne la décomposition modulaire ; permet un déploiement indépendant
OCPL'ajout de fonctionnalités nécessite de modifier le code testé ; croissance des chaînes if/elseDépendez des abstractions ; étendre en ajoutant de nouvelles implémentationsArchitectures de plugins Drives, modèle de stratégie, points d'extension
LSPSous-classes qui interrompent l'appel du code ; UnsupportedOperationException dans les remplacementsHonorez le contrat comportemental, pas seulement la signatureAssure la substituabilité dans les hiérarchies polymorphes ; détermine la composition plutôt que l'héritage
FAIMettre en œuvre des méthodes non pertinentes ; grosses interfaces qui changent trop souventPetites interfaces spécifiques aux rôles par besoin du clientPilote les interfaces de rôle, réduit le couplage entre les modules
TREMPAGELe code de haut niveau dépend de la base de données/du framework ; intestable sans infrastructureDéfinir les abstractions dans un package de haut niveau ; injecter des détailsFondation de l’architecture oignon/hexagonale/propre ; permet de tester les doubles
Mauvaises applications courantes de SOLID
Cliquez sur chaque carte : l'application mécanique de SOLID entraîne ses propres problèmes
À retenir : les principes SOLID abordent cinq modes de défaillance concrets de la conception orientée objet. SRP empêche le couplage multi-acteurs. OCP empêche la modification du code testé. LSP empêche les violations comportementales des contrats dans les hiérarchies. Le FAI empêche le couplage graisse-interface. DIP empêche le code de haut niveau de dépendre de détails de bas niveau. Appliquez-les là où le symptôme est présent – ​​et non comme modèle universel pour chaque classe.
Et ensuite : SOLID vous explique comment attribuer des responsabilités au sein d'une classe et comment les classes doivent dépendre les unes des autres. GRASP fournit un ensemble complémentaire de modèles spécifiquement axés sur la question de savoir quel objet doit être responsable de quoi. La leçon suivante couvre les neuf modèles GRASP avec des exemples.