Leçon 8/8 12 min

Bien concevoir : SOLID

Cinq boussoles pour un code propre. Pas un dogme : des angles de lecture avec un avant/après pour chaque principe.

Cinq lettres, cinq boussoles

Tu sais maintenant créer des objets, protéger leurs données, hériter, composer et écrire des contrats. Il reste une question. Quand ton code grossit, comment décider où va quoi ? Quelle classe porte quelle responsabilité ? Où tracer la frontière ?

SOLID, ce sont cinq principes résumés par cinq lettres, les initiales de leurs noms anglais. Ce n'est pas un dogme à appliquer aveuglément. Vois-les comme cinq boussoles : cinq angles pour relire ton code et repérer ce qui va te faire souffrir plus tard. Pour chaque principe, on reprend le même exemple : le Notifier de la leçon 7.

Les cinq lettres de SOLID, chacune dans une pastille avec son nom anglais : S pour Single responsibility (responsabilité unique), O pour Open/Closed (ouvert/fermé), L pour Liskov substitution, I pour Interface segregation (ségrégation d'interface), D pour Dependency inversion (inversion de dépendance). S Single responsibility O Open / Closed L Liskov substitution I Interface segregation D Dependency inversion
Cinq boussoles pour relire ton code, pas cinq règles à réciter.
Prédisez avant de lire

Une classe Commande fait tout : elle calcule le total, l'enregistre en base, et envoie l'e-mail de confirmation. Avant de dérouler : combien de raisons différentes cette classe a-t-elle de changer ? Et en quoi est-ce un problème quand l'équipe grandit ?

Voir la réponse

Au moins trois : la règle de calcul du total peut changer (promos, TVA), la façon d'enregistrer peut changer (base SQL → API), le canal de notification peut changer (e-mail → SMS). Trois sujets, trois équipes potentielles, trois raisons de rouvrir la même classe. Résultat : tout le monde se marche dessus, un changement de TVA risque de casser l'envoi d'e-mail. C'est exactement ce que le S de SOLID veut éviter : une classe = une seule raison de changer.

S : une classe, une seule raison de changer

Le S (Single responsibility, responsabilité unique) dit : une classe ne devrait avoir qu'une seule raison de changer. Si trois changements sans rapport t'obligent à la rouvrir, elle fait trois métiers à la fois. Sépare-les.

// ❌ Avant : Commande calcule, enregistre ET notifie
classe Commande:
    total(): ...                 // change si les règles de prix changent
    enregistrer(): ...           // change si la base change
    envoyerConfirmation(): ...   // change si le canal change

// ✅ Après : une responsabilité par classe
classe Commande:        total(): ...                  // juste le métier
classe CommandeRepository: enregistrer(commande): ... // juste la persistance
classe Inscription:     notifier.envoyer(...)         // juste la notification

Chaque classe n'a plus qu'une seule raison de changer. Toucher le calcul du prix ne risque plus de casser l'envoi d'e-mail.

O : ouvert à l'extension, fermé à la modification

Le O (Open/Closed, ouvert/fermé) dit : on doit pouvoir ajouter un cas sans modifier le code existant. C'est exactement ce que l'interface de la leçon 7 t'a apporté. Une cascade de if sur le type viole ce principe. Un contrat le respecte.

// ❌ Avant : ajouter un canal = rouvrir et modifier cette fonction
fonction notifier(canal, msg):
    si canal == "email": ...
    sinon si canal == "sms": ...     // un else if de plus à chaque canal

// ✅ Après : chaque canal implémente Notifier ; on BRANCHE, on ne modifie pas
classe SlackNotifier implémente Notifier:
    public envoyer(msg): ...         // nouveau canal, zéro ligne touchée ailleurs

Le O est le prolongement direct de la leçon 7. Dépendre d'un contrat, c'est ce qui rend le code ouvert à l'extension. Un nouveau cas se branche, l'ancien code ne bouge pas.

L : un enfant remplace son parent sans surprise

Le L (Liskov substitution, substitution de Liskov) dit : partout où le code attend un parent, on doit pouvoir glisser un enfant sans rien casser. C'est le rappel direct de l'héritage (leçon 4) : si l'enfant trahit la promesse du parent, l'héritage devient un piège.

// ❌ Avant : Manchot hérite d'Oiseau mais casse la promesse voler()
classe Oiseau:   public voler(): ...        // tout Oiseau sait voler
classe Manchot hérite Oiseau:
    public voler(): lever_erreur("je ne vole pas !")  // surprise : ça plante

// ✅ Après : on ne promet voler() qu'à ceux qui tiennent la promesse
classe Oiseau: ...
classe OiseauVolant hérite Oiseau:  public voler(): ...
classe Manchot     hérite Oiseau:   // pas de voler() : aucune promesse trahie

Le piège du L : un enfant qui hérite d'une méthode juste pour la désactiver (lever une erreur, ne rien faire) trahit le contrat du parent. Le code qui faisait confiance au parent plante avec l'enfant. Si Manchot ne sait pas voler, il ne doit pas hériter d'un voler().

I : plusieurs petits contrats valent mieux qu'un gros

Le I (Interface segregation, ségrégation d'interface) dit : mieux vaut plusieurs petites interfaces ciblées qu'une seule énorme. Personne ne devrait être forcé d'implémenter des méthodes dont il n'a pas besoin. C'est le prolongement de la leçon 7 : un contrat doit promettre juste ce qu'il faut, rien de plus.

// ❌ Avant : un gros contrat fourre-tout
interface Machine:
    imprimer(doc)
    scanner(doc)
    faxer(doc)
// La vieille imprimante qui ne fait QUE imprimer doit quand même
// fournir scanner() et faxer()... vides ou qui plantent.

// ✅ Après : des petits contrats ciblés, on n'implémente que ce qu'on tient
interface Imprimante:  imprimer(doc)
interface Scanner:     scanner(doc)
classe VieilleImprimante implémente Imprimante:  public imprimer(doc): ...

D : dépendre du contrat, pas du concret

Le D (Dependency inversion, inversion de dépendance) dit : ton code de haut niveau doit dépendre d'un contrat, pas d'une classe concrète. C'est exactement la règle de la leçon 7, « dépendre du contrat, pas de l'implémentation », devenue un principe.

// ❌ Avant : Inscription fabrique elle-même un EmailNotifier concret
classe Inscription:
    privé notifier = new EmailNotifier()   // soudé à l'e-mail

// ✅ Après : Inscription reçoit un Notifier (le contrat), on lui injecte
classe Inscription:
    constructeur(notifier):                // un Notifier, peu importe lequel
        this.notifier = notifier

Le service (Inscription) et le détail technique (EmailNotifier) dépendent maintenant du même contrat Notifier. Plus rien n'est soudé. Tu peux tester Inscription avec un faux notifier ou changer de canal, sans jamais rouvrir le service.

Vérifie ce principe en vrai JavaScript. Inscription ne dépend que du contrat. On lui injecte un vrai notifier, puis un faux pour les tests, sans changer une ligne du service.

class EmailNotifier {
  envoyer(message) { return "📧 envoyé : " + message; }
}
// Un faux notifier pour tester, qui respecte le même contrat
class FakeNotifier {
  constructor() { this.recus = []; }
  envoyer(message) { this.recus.push(message); return "(capturé)"; }
}

class Inscription {
  constructor(notifier) { this.notifier = notifier; }
  sInscrire(user) { return this.notifier.envoyer("Bienvenue " + user); }
}

console.log(new Inscription(new EmailNotifier()).sInscrire("Alice"));

// En test : on injecte le faux, on vérifie ce qui a été envoyé
const faux = new FakeNotifier();
new Inscription(faux).sInscrire("Bob");
console.log("Messages capturés :", faux.recus);

SOLID n'est pas une religion

Le vrai piège, c'est le dogme. Appliquer SOLID à fond sur un petit script de 30 lignes est une erreur, au même titre que l'ignorer sur une grosse application. Découper une classe minuscule en cinq interfaces, injecter dix dépendances pour afficher « Bonjour » : tu fabriques de la complexité au lieu de l'éviter. SOLID répond à un coût réel (le code qui devient dur à changer). Pas de coût, pas besoin du remède.

La bonne posture : connaître les cinq boussoles, et les sortir quand la douleur arrive. Une classe qu'on rouvre sans cesse pour des raisons sans rapport ? Le S t'aide. Un if sur le type qui grossit à chaque feature ? Le O et le D t'aident. Tant que le code reste simple et stable, laisse-le simple.

Ne cherche pas à « faire du SOLID ». Écris ton code le plus simple possible, puis quand un changement devient pénible, demande-toi quelle boussole pointe le problème. SOLID est un outil de diagnostic, pas une checklist à cocher avant d'écrire.

La boucle est bouclée

Tu es parti du code spaghetti (leçon 1) : des données d'un côté, des fonctions de l'autre, tout emmêlé. Tu as appris à ranger ce désordre dans des objets, à protéger leurs données, à les réutiliser par héritage et composition, à les rendre interchangeables par le polymorphisme et les contrats. SOLID est la dernière pièce : cinq boussoles pour décider où va quoi quand le code grandit.

C'était la promesse du début : comprendre la POO comme une façon de penser, pas comme une syntaxe. C'est fait, et ça vaut pour tous les langages. La suite logique : l'appliquer pour de vrai, dans un langage précis, où tu écris des classes que tu exécutes. Direction PHP orienté objet.

Five letters, five compasses

You now know how to build objects, encapsulate them, make them inherit, compose them, make them polymorphic, and write contracts. One question remains: when your code grows, how do you decide where things go? Which class carries which responsibility? Where do you draw the line?

SOLID is five principles summed up by five letters. It is not a dogma to apply blindly. See them as five compasses: five angles to re-read your code and spot what will hurt you later. We reuse the same thread, the Notifier from lesson 7, for each one.

The five letters of SOLID, each in a badge: S for single responsibility, O for open/closed, L for Liskov substitution, I for interface segregation, D for dependency inversion. S Single responsibility O Open / Closed L Liskov substitution I Interface segregation D Dependency inversion
Five compasses to re-read your code, not five rules to recite.
Predict before reading

A Commande (order) class does everything: it computes the total, saves it to the database, and sends the confirmation e-mail. Before you expand: how many distinct reasons does this class have to change over time? And why is that a problem as the team grows?

See the answer

At least three: the total-computing rule can change (promos, VAT), the way it's saved can change (SQL database → API), the notification channel can change (e-mail → SMS). Three topics, three potential teams, three reasons to reopen the same class. Result: everyone steps on each other, a VAT change risks breaking the e-mail sending. That's exactly what the S of SOLID avoids: one class = one single reason to change.

S — one class, one single reason to change

The S (single responsibility) says: a class should have only one reason to change. If you can break it for three unrelated reasons, it does three jobs. Split them.

// ❌ Before: Commande computes, saves AND notifies
class Commande:
    total(): ...                 // changes if pricing rules change
    enregistrer(): ...           // changes if the database changes
    envoyerConfirmation(): ...   // changes if the channel changes

// ✅ After: one responsibility per class
class Commande:           total(): ...                   // just the business
class CommandeRepository: enregistrer(commande): ...     // just persistence
class Inscription:        notifier.envoyer(...)          // just notification

Each class now has one single reason to move. Touching the price calculation no longer risks breaking the e-mail sending.

O — open to extension, closed to modification

The O says: you should be able to add a case without modifying existing code. That's exactly what the interface from lesson 7 gave you. A cascade of if on the type violates the O; a contract honors it.

// ❌ Before: adding a channel = reopen and edit this function
function notifier(canal, msg):
    if canal == "email": ...
    else if canal == "sms": ...      // one more else if per channel

// ✅ After: each channel implements Notifier; you PLUG IN, you don't edit
class SlackNotifier implements Notifier:
    public envoyer(msg): ...         // new channel, zero lines touched elsewhere

The O is the direct extension of lesson 7. Depending on a contract is what makes code open to extension. A new case plugs in; the old code doesn't move.

L — a child replaces its parent without surprise

The L (Liskov substitution) says: anywhere the code expects a parent, you should be able to slip in a child without breaking anything. It's the direct callback to inheritance (lesson 4): if the child betrays the parent's promise, inheritance is a trap.

// ❌ Before: Manchot inherits Oiseau but breaks the voler() promise
class Oiseau:   public voler(): ...        // every Oiseau can fly
class Manchot inherits Oiseau:
    public voler(): raise_error("I don't fly!")  // surprise: it crashes

// ✅ After: only promise voler() to those who keep the promise
class Oiseau: ...
class OiseauVolant inherits Oiseau:  public voler(): ...
class Manchot      inherits Oiseau:  // no voler(): no promise betrayed

The L trap: a child that inherits a method just to disable it (raise an error, do nothing) betrays the parent's contract. The code that trusted the parent crashes with the child. If Manchot (penguin) can't fly, it must not inherit a voler().

I — several small contracts beat one big one

The I (interface segregation) says: better several small, targeted interfaces than one huge one. Nobody should be forced to implement methods they don't need. It's the extension of lesson 7: a contract must stay fair.

// ❌ Before: one big catch-all contract
interface Machine:
    imprimer(doc)
    scanner(doc)
    faxer(doc)
// The old printer that ONLY prints must still
// provide scanner() and faxer()... empty or crashing.

// ✅ After: small targeted contracts, you only implement what you keep
interface Imprimante:  imprimer(doc)
interface Scanner:     scanner(doc)
class VieilleImprimante implements Imprimante:  public imprimer(doc): ...

D — depend on the contract, not the concrete

The D (dependency inversion) says: your high-level code must depend on a contract, not a concrete class. It's exactly the "code against the interface" of lesson 7, raised to a principle.

// ❌ Before: Inscription builds a concrete EmailNotifier itself
class Inscription:
    private notifier = new EmailNotifier()   // welded to e-mail

// ✅ After: Inscription receives a Notifier (the contract), you inject it
class Inscription:
    constructor(notifier):                   // a Notifier, whichever it is
        this.notifier = notifier

The high-level code (Inscription) and the low-level code (EmailNotifier) both depend on the same contract Notifier, instead of one being welded to the other. You can test Inscription with a fake notifier, switch channels, all without reopening the service.

Check this principle in real JavaScript. Inscription only depends on the contract; you inject a real notifier, then a fake one for tests, without changing a line of the service.

class EmailNotifier {
  envoyer(message) { return "📧 sent: " + message; }
}
// A fake notifier for testing, honoring the same contract
class FakeNotifier {
  constructor() { this.recus = []; }
  envoyer(message) { this.recus.push(message); return "(captured)"; }
}

class Inscription {
  constructor(notifier) { this.notifier = notifier; }
  sInscrire(user) { return this.notifier.envoyer("Welcome " + user); }
}

console.log(new Inscription(new EmailNotifier()).sInscrire("Alice"));

// In a test: inject the fake, check what was sent
const faux = new FakeNotifier();
new Inscription(faux).sInscrire("Bob");
console.log("Captured messages:", faux.recus);

SOLID is not a religion

The real trap is dogma. Applying SOLID fully to a tiny 30-line script is just as much a mistake as ignoring it on a big application. Splitting a tiny class into five interfaces, injecting ten dependencies to print "Hello": you manufacture complexity instead of avoiding it. SOLID answers a real cost (code that becomes hard to change). No cost, no need for the cure.

The right posture: know the five compasses, and pull them out when the pain arrives. A class you keep reopening for unrelated reasons? The S helps. An if on the type that grows with every feature? The O and D help. As long as it stays simple and stable, leave it simple.

Don't try to "do SOLID". Write the simplest code you can, then when a change becomes painful, ask yourself which compass points at the problem. SOLID is a diagnostic tool, not an entry checklist.

Full circle

You started from spaghetti code (lesson 1): data on one side, functions on the other, everything tangled. You learned to tidy that mess into objects, to protect their data, to reuse them through inheritance and composition, to make them interchangeable through polymorphism and contracts. SOLID is the last piece: five compasses to decide where things go as code grows.

That was the promise of the index: understand OOP as a way of thinking, not a syntax. You hold it now, independently of any language. The logical next step: apply it for real, in a specific language, where you write classes you actually run. Off to object-oriented PHP.

🎯 Pratique

S'entraîner (clique pour ouvrir) :

🧠 Rappel libre
Rappel libre

Sans remonter dans la leçon : que veulent dire le S, le O et le D de SOLID, et pourquoi est-ce une faute d'appliquer SOLID à fond sur un petit script ?

Le S (Single responsibility) : une classe ne doit avoir qu'une seule raison de changer. Le O (Open/Closed) : on doit pouvoir ajouter un cas sans modifier l'existant, grâce à un contrat. Le D (Dependency inversion) : dépendre du contrat, pas d'une classe concrète. Appliquer SOLID à fond sur un petit script est une faute parce qu'on fabrique alors de la complexité inutile (interfaces et injections pour rien) : SOLID répond à un coût réel (le code dur à changer) ; pas de coût, pas besoin du remède.
⚖️ Juge le code de l'IA
Accepter ou rejeter le code de l'IA

Tu écris un petit script perso qui salue l'utilisateur par son prénom. L'IA, « pour faire propre », te propose ce design SOLID. Ton rôle de relecteur : l'accepter tel quel ou le rejeter, et dire pourquoi.

interface Salueur { saluer(nom); }
interface FormateurNom { formater(nom); }
class FormateurNomDefaut { formater(nom) { return nom.trim(); } }
class SalueurFr {
  constructor(formateur) { this.formateur = formateur; }
  saluer(nom) { return "Bonjour " + this.formateur.formater(nom); }
}
// usage : new SalueurFr(new FormateurNomDefaut()).saluer("  Alice ")
À rejeter, et c'est l'inverse du piège habituel : ici l'IA sur-architecture. Pour afficher « Bonjour Alice », elle invente deux interfaces, une classe formateur et une injection de dépendance. Le code juste tient en une ligne : const saluer = nom => "Bonjour " + nom.trim();. SOLID répond à un coût réel (un code qu'on rouvre sans cesse, des if qui gonflent) ; sur un script trivial, ce coût n'existe pas, donc tout ce découpage n'ajoute que de la complexité. Le pragmatisme prime : pas de douleur, pas de remède.
Que dit le S (responsabilité unique) de SOLID ?
Le O (ouvert/fermé) prolonge directement quelle notion vue avant ?
Une classe Manchot hérite de Oiseau et redéfinit voler() pour lever une erreur « je ne vole pas ». Quel principe SOLID est violé, et pourquoi ?
Quelle est la bonne posture face à SOLID, sur un petit script de 30 lignes ?
Prochaine étape

Tu tiens la POO comme une façon de penser, indépendamment du langage : objet, encapsulation, héritage, composition, polymorphisme, interfaces et SOLID. Place à la pratique en PHP, où tu écris de vraies classes que tu exécutes.

PHP orienté objet →

Une erreur dans cette leçon, un passage flou, une question ? Écrivez-moi : chaque retour améliore ce cours.

Besoin d'un développeur pour votre projet ?

Réponse sous 24h · Sans engagement