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.
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.
« 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.