Aller au contenu principal

Concept #009

Architecture

Les 5 Principes SOLID

#0098 min de lecture
  • architecture
  • clean code
  • design patterns
  • SOLID
5 piliers pour un code robuste, flexible et maintenable

Les principes SOLID en une phrase

SOLID est un acronyme représentant 5 principes fondamentaux de la programmation orientée objet, formulés par Robert C. Martin (Uncle Bob), qui guident les développeurs vers un code plus robuste, plus flexible et plus facile à maintenir.

Souvent cités, rarement maîtrisés. Ce ne sont pas des dogmes religieux, mais des outils puissants pour éviter que votre base de code ne devienne un plat de spaghettis géant.


S — Single Responsibility Principle (SRP)

Une classe ne devrait avoir qu'une seule et unique raison de changer.

Illustration du principe SRP : avant avec une God Class qui fait tout, après avec des responsabilités séparées en Service Métier, Repository et Presenter/View

Le problème : la "God Class"

On l'a tous vu (ou écrit) : cette classe "couteau suisse" qui gère à la fois la logique métier, la persistance en base de données, l'envoi de notifications Slack, la comptabilité et le nettoyage de la base. C'est le fameux SUPER_MANAGER.

Le souci ? Si une seule de ces responsabilités change, tout risque de casser. Modifier l'envoi de mails peut introduire un bug dans la logique métier. Un changement de base de données impacte l'affichage. Tout est couplé.

La solution : séparer le "QUOI" du "COMMENT"

Pensez à un manager qui délègue : le Use Case gère le QUOI (la logique métier), et les adaptateurs gèrent le COMMENT (persistance, affichage, notifications). Chacun son job.

Concrètement, séparez en 3 responsabilités distinctes :

  • Le Métier (Service Métier) — Ne gère que les règles business
  • La Persistance (Repository) — Ne gère que le stockage des données
  • L'Affichage (Presenter / View) — Ne gère que la mise en forme pour l'utilisateur

Indicateur de violation

Si vous décrivez ce que fait votre classe et que vous utilisez le mot "ET", c'est un signal d'alarme : "Cette classe gère les utilisateurs ET envoie des emails ET génère des rapports" → 3 classes distinctes.


O — Open/Closed Principle (OCP)

Ouvert à l'extension, fermé à la modification.

Illustration du principe OCP : avant avec une modification directe risquée d'un monolithe, après avec un système d'extensions modulaires

Le problème : le pied-de-biche dans le code stable

Chaque fois qu'on veut ajouter une fonctionnalité, on "ouvre" le code existant au pied-de-biche pour y insérer la nouvelle feature. Modifier du code stable qui fonctionne, c'est risquer de casser des fonctionnalités existantes en production.

La solution : les points d'extension

Au lieu de modifier le code existant, on crée un point d'extension (une interface) et on branche un nouveau module sans toucher au code stable.

Le Cœur du Système (Fermé à la modification) :

  • Le module stable (Core) est verrouillé, on n'y touche plus
  • Il expose un point d'extension via une interface

L'Extension (Ouverte) :

  • On ajoute simplement un nouveau module qui implémente l'interface
  • Exemple : un système de paiement avec une interface PaymentProcessor. Pour ajouter le paiement Crypto, on crée un nouveau module CryptoPayment sans modifier PaypalPayment ni CardPayment

En pratique

Utilisez le Strategy Pattern, le Plugin Pattern ou l'injection de dépendances pour rendre votre code extensible sans modification. Pensez aux plugins de votre IDE : on en ajoute sans jamais modifier le code de l'éditeur lui-même.


L — Liskov Substitution Principle (LSP)

Les sous-classes doivent être interchangeables avec leur classe parente sans mauvaise surprise.

Illustration du principe LSP : une machine à café comme contrat de base, une machine à thé qui viole le contrat, et une machine à espresso qui le respecte

Le problème : les mauvaises surprises

Si le contrat parent dit "Machine à Café", la classe enfant n'a pas le droit de servir du thé. Le code appelant s'attend à recevoir un café de toute machine qui respecte ce contrat. L'héritage est une promesse de comportement, pas juste du partage de code.

L'analogie de la machine à café

  • Le contrat de base : MachineACafé — "Je te sers un café quand tu appuies sur ce bouton"
  • Violation : MachineAThé hérite de MachineACafé mais sert du thé → le code qui attend un café est cassé
  • Respect : MachineAEspresso hérite de MachineACafé et sert bien un café (espresso) → le contrat est respecté

La règle d'or

Une sous-classe ne doit jamais :

  • Renforcer les préconditions (exiger plus que la classe parente)
  • Affaiblir les postconditions (promettre moins que la classe parente)
  • Lever des exceptions inattendues que la classe parente ne lève pas

Si vous devez écrire if (instance is SubClass) dans le code appelant, c'est que LSP est violé. Le polymorphisme devrait suffire.


I — Interface Segregation Principle (ISP)

Préférez plusieurs interfaces spécifiques plutôt qu'une seule interface généraliste.

Illustration du principe ISP : avant avec une interface monolithique fourre-tout, après avec des interfaces séparées pour voler, nager et calculer

Le problème : l'interface fourre-tout

Pourquoi forcer une classe Poisson à implémenter une méthode voler() qu'elle n'utilisera jamais ? Une interface universelle "tout-en-un" (voler, nager, imprimer, manger, calculer, danser...) force les clients à dépendre de méthodes qu'ils n'utilisent pas. C'est du gaspillage et une source de bugs.

La solution : la ségrégation

Au lieu d'une méga-interface monolithique, découpez en interfaces ciblées :

  • Interface Volant — Battement des ailes, direction → Ne gère que le vol
  • Interface Nageur — Gouvernail, nageoires → Ne gère que la nage
  • Interface Calculateur — Opérations mathématiques → Ne gère que le calcul

Chaque classe n'implémente que les interfaces dont elle a réellement besoin. Un poisson implémente Nageur mais pas Volant. Un oiseau implémente Volant mais pas Nageur (sauf le canard, qui fait les deux).

En pratique

Quand une interface grossit, posez-vous la question : "Est-ce que tous les implémenteurs utilisent toutes les méthodes ?" Si non, il est temps de découper. En TypeScript/Java, préférez composer plusieurs petites interfaces plutôt qu'en hériter une seule grosse.


D — Dependency Inversion Principle (DIP)

Les modules de haut niveau ne doivent pas dépendre des modules de bas niveau. Les deux doivent dépendre d'abstractions.

Illustration du principe DIP : avant avec un couplage fort entre le pilote et le moteur, après avec une abstraction d'interface Véhicule Conduisible

Le problème : le couplage fort

Imaginez un pilote qui, pour tourner, doit "tirer le câble B42 et ajuster la valve vapeur". Ce pilote connaît trop de détails du moteur spécifique. Impossible de changer de véhicule sans réapprendre tout le fonctionnement interne.

C'est exactement ce qui se passe quand votre logique métier dépend directement de votre base de données, de votre framework HTTP ou de votre service d'envoi de mails.

La solution : l'inversion de dépendance

Introduisez une abstraction (interface) entre le haut niveau et le bas niveau :

  • Le Haut Niveau (Le Pilote) — "Je connais le volant et les pédales. Le reste, je m'en fiche !" → Dépend de l'interface Commandes
  • L'Abstraction (Le Contrat Standard) — Interface VéhiculeConduisible → Le contrat que tout véhicule doit respecter
  • Le Bas Niveau (Les Véhicules) — Voiture, tracteur, hovercraft → Les constructeurs s'adaptent au standard pour être pilotables

Pourquoi "inversion" ?

Sans ce principe, la dépendance va naturellement du haut vers le bas : Service → BaseDeDonnées. Avec DIP, on inverse : Service → Interface ← BaseDeDonnées. Les deux pointent vers l'abstraction. Résultat : on peut changer la base de données sans toucher au service.

En pratique

C'est le fondement de l'Architecture Hexagonale (Ports & Adapters) et du Clean Architecture. Votre domaine métier est au centre, protégé par des interfaces (ports). Les détails techniques (base de données, API, UI) sont à l'extérieur (adapters) et peuvent être remplacés à volonté.


Résumé : les 5 principes à retenir

PrincipeAcronymeEn une phrase
Single ResponsibilitySRPUne classe = une seule raison de changer
Open/ClosedOCPOuvert à l'extension, fermé à la modification
Liskov SubstitutionLSPLes sous-classes respectent le contrat du parent
Interface SegregationISPPlein de petites interfaces > une grosse
Dependency InversionDIPDépendez des abstractions, pas des détails

Quand appliquer SOLID ?

Ces principes ne sont pas à appliquer aveuglément sur chaque ligne de code. Ils prennent tout leur sens dans les projets qui grandissent, où la maintenabilité et l'évolutivité deviennent critiques. Pour un script de 50 lignes, SOLID serait de la sur-ingénierie. Pour une application métier qui vivra des années, c'est la différence entre un code qui évolue sereinement et un code qu'on a peur de toucher.