SOLID

Les principes SOLID, enfin clairs

5 principes pour un code orienté objet maintenable, testable et évolutif. Avec des analogies du quotidien, du code avant/après, et le pourquoi de chacun.

Pourquoi s'embêter avec SOLID ? Formalisés par Robert C. Martin, ces 5 principes ont un seul objectif : réduire le coût du changement. Sans eux, le code devient rigide (on a peur d'y toucher), fragile (un changement en casse trois) et difficile à tester. Avec eux, on livre plus vite et plus sereinement — surtout sur la durée et en équipe. Ce ne sont pas des dogmes, mais des réflexes qui paient à chaque refactoring.

SOLID en un coup d'œil

Les 5 principes côte à côte : une lettre, l'idée en une phrase, et un mini-schéma. À gauche, ce qu'on évite ; à droite, le bon réflexe.

S Responsabilité unique une classe, une seule raison de changer 3 responsabilités 1 responsabilité O Ouvert / Fermé ouvert à l'extension, fermé à la modif. Cœur fermé Extension + Extension + on étend sans toucher au cœur L Substitution de Liskov un dérivé remplace sa base sans casser Base Dérivé code appelant { … } le dérivé tient la place de la base I Ségrégation des interfaces plusieurs interfaces fines et ciblées fourre-tout Imprimer Scanner Faxer contrats fins D Inversion des dépendances dépendre d'abstractions, pas du concret « Interface » abstraction Module Implém. tout dépend de l'abstraction

SOLID = 5 principes pour un code à faible couplage et facile à faire évoluer : une responsabilité par classe, on étend sans modifier, les sous-types restent substituables, des interfaces fines, et on dépend d'abstractions.

S

Responsabilité uniqueSingle Responsibility Principle

« Une classe ne devrait avoir qu'une seule raison de changer. »

L'analogie — Un couteau suisse fait tout, mais mal. Un bon outil fait une seule chose, parfaitement.
// Une classe qui fait 3 métiers à la fois
class Rapport {
  generer() { /* ... */ }
  sauvegarderEnBase() { /* logique SQL */ }
  envoyerParEmail() { /* logique SMTP */ }
}
// Chaque classe a UNE responsabilité
class Rapport { generer() { /* ... */ } }
class RapportRepository { sauvegarder(r) { /* SQL */ } }
class EmailService { envoyer(r) { /* SMTP */ } }
Pourquoi — Changer de base de données ne touche plus la génération du rapport. Chaque classe se teste isolément, et les changements restent localisés : moins de régressions.
O

Ouvert / FerméOpen/Closed Principle

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

L'analogie — Une multiprise : vous branchez un nouvel appareil sans refaire l'installation électrique.
// Ajouter une forme = MODIFIER cette fonction
function aire(forme) {
  if (forme.type === 'cercle') return Math.PI * forme.r * forme.r;
  if (forme.type === 'carre')  return forme.cote * forme.cote;
  // ... et un triangle ? on revient tout casser ici
}
// Ajouter une forme = CRÉER une classe, sans rien toucher
class Cercle { constructor(r) { this.r = r; } aire() { return Math.PI * this.r * this.r; } }
class Carre  { constructor(c) { this.c = c; } aire() { return this.c * this.c; } }
class Triangle { /* nouveau, isolé */ aire() { /* ... */ } }
Pourquoi — Vous ajoutez des fonctionnalités sans risquer de casser le code existant et déjà testé. L'extension devient sûre.
L

Substitution de LiskovLiskov Substitution Principle

« Un sous-type doit pouvoir remplacer son type parent sans changer le comportement attendu. »

L'analogie — Si c'est étiqueté « café », peu importe la marque : ça doit rester du café buvable.
class Oiseau { voler() { /* ... */ } }
class Autruche extends Oiseau {
  voler() { throw new Error("Je ne vole pas !"); } // surprise !
}
// Du code qui attend un Oiseau.voler() casse avec une Autruche
class Oiseau { /* comportements communs */ }
class OiseauVolant extends Oiseau { voler() { /* ... */ } }
class Autruche extends Oiseau { courir() { /* ... */ } }
// On ne promet « voler » que là où c'est vrai
Pourquoi — Le polymorphisme devient fiable : aucune mauvaise surprise quand on substitue une implémentation à une autre.
I

Ségrégation des interfacesInterface Segregation Principle

« Mieux vaut plusieurs interfaces spécifiques qu'une seule interface fourre-tout. »

L'analogie — Un menu à la carte, plutôt qu'un menu unique imposé où vous payez des plats que vous ne mangez pas.
// Une interface qui force des méthodes inutiles
class Employe { travailler() {} mangerCantine() {} }
class Robot extends Employe {
  travailler() { /* ... */ }
  mangerCantine() { // ??? un robot ne mange pas }
}
// Des contrats fins, on n'implémente que le nécessaire
class Travailleur { travailler() {} }
class Mangeur { mangerCantine() {} }
class Robot extends Travailleur { travailler() { /* ... */ } }
Pourquoi — Aucune classe n'est forcée d'implémenter des méthodes qui n'ont aucun sens pour elle. Les contrats restent honnêtes.
D

Inversion des dépendancesDependency Inversion Principle

« Dépends d'abstractions, pas d'implémentations concrètes. »

L'analogie — Une lampe se branche sur une prise standard. Elle n'est pas soudée directement au câble du mur.
class ServiceCommande {
  constructor() { this.db = new MySQL(); } // dépendance en dur
  enregistrer() { this.db.insert(/* ... */); }
}
// Impossible de tester sans une vraie base MySQL
class ServiceCommande {
  constructor(db) { this.db = db; } // injectée (abstraction)
  enregistrer() { this.db.insert(/* ... */); }
}
// On injecte MySQL, Postgres, ou un faux pour les tests
Pourquoi — Testable (on injecte un mock), flexible (on change de techno sans réécrire la logique métier). C'est la base de l'injection de dépendances.

🎯 Testez-vous

Pour chaque situation, quel principe SOLID est violé ?

1. Une classe Utilisateur valide les données, les enregistre en base et envoie l'email de bienvenue.
S — Responsabilité unique. Trois responsabilités dans une classe : sépare validation, persistance et envoi d'email.
2. Pour ajouter un nouveau moyen de paiement, il faut rouvrir et modifier un gros switch (type) existant.
O — Ouvert/Fermé. On devrait ajouter une classe de paiement, pas modifier l'existant.
3. class Service { constructor() { this.db = new MongoDB(); } }
D — Inversion des dépendances. Le service est soudé à MongoDB. Injecte une abstraction de base de données.
4. Une interface Imprimante impose imprimer(), scanner() et faxer() — même à une imprimante qui ne sait qu'imprimer.
I — Ségrégation des interfaces. Découpe en contrats fins : Imprimable, Scannable, Faxable.
5. Carre hérite de Rectangle, mais changer la largeur modifie aussi la hauteur — cassant le code qui manipule un Rectangle.
L — Substitution de Liskov. Un Carre ne se comporte pas comme un Rectangle : l'héritage est trompeur ici.