Chapitre 5.4.10Les principes SOLID

Cinq repères pour un code qui encaisse le changement.

6 minutes de lecture

Dans le chapitre sur les design patterns, on a vu des recettes toutes prêtes pour des problèmes précis. SOLID joue un autre rôle. Ce n'est pas une recette, mais cinq principes de conception qui aident à juger si un code orienté objet tiendra le choc quand le métier évoluera.

La seule façon d'aller vite, c'est de bien faire les choses. <Robert C. MARTIN>

D'où vient SOLID ?

Les idées sont plus anciennes que le sigle. En 1987, l'informaticienne américaine Barbara Liskov présente le principe qui porte aujourd'hui son nom. L'année suivante, le Français Bertrand Meyer formule le principe ouvert/fermé dans un livre sur la conception orientée objet.

En 2000, Robert C. Martin, surnommé "Uncle Bob" dans le milieu, rassemble ces règles et quelques autres dans un article devenu une référence. Quelques années plus tard, Michael Feathers remarque que leurs initiales forment le mot SOLID, "solide" en anglais. Le sigle est resté, en grande partie parce qu'il se retient facilement.

Les cinq lettres
S   Single responsibility     Responsabilité unique
O   Open/closed               Ouvert/fermé
L   Liskov substitution       Substitution de Liskov
I   Interface segregation     Ségrégation des interfaces
D   Dependency inversion      Inversion des dépendances

S, la responsabilité unique

Un morceau de code ne devrait avoir qu'une seule raison de changer. Autrement dit, il ne devrait rendre des comptes qu'à un seul métier.

Imagine une classe Commande qui calcule le total, génère la facture en PDF et envoie l'email de confirmation. Elle a trois raisons de changer. La comptabilité veut modifier un arrondi de TVA, le design veut refaire la facture, le marketing veut réécrire l'email. Trois équipes, trois demandes, un seul fichier, et chaque modification risque de casser les deux autres.

La correction consiste à séparer ces responsabilités en trois classes, une pour le calcul du total, une pour la facture, une pour l'email. Chacune évolue à son rythme, sans risquer d'abîmer ses voisines.

C'est l'antidote direct à l'objet fourre-tout, cette classe de trois mille lignes que tout le monde évite d'ouvrir. Quand un développeur dit "cette classe fait trop de choses", c'est ce principe qu'il invoque.

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

On doit pouvoir ajouter un comportement sans modifier le code qui fonctionne déjà. Le code est ouvert aux ajouts, mais fermé aux retouches.

Prenons le calcul des frais de port d'une boutique en ligne. La version naïve empile les conditions.

Chaque transporteur ajouté modifie la classe
class CalculFraisDePort
{
    public function calculer(Colis $colis, string $transporteur): float
    {
        if ($transporteur === 'colissimo') {
            return 4.90 + $colis->poids * 0.5;
        }

        if ($transporteur === 'chronopost') {
            return 12.00 + $colis->poids * 0.8;
        }

        // Et demain, un nouveau bloc pour chaque transporteur

        throw new Exception('Transporteur inconnu');
    }
}

Chaque nouveau transporteur oblige à rouvrir cette classe, à la retester en entier, et à prendre le risque de casser les tarifs existants.

La version qui respecte le principe s'appuie sur une interface. Une interface est un contrat, qui liste ce qu'une classe s'engage à savoir faire sans dire comment elle le fait. Chaque transporteur a sa propre classe, qui respecte ce contrat commun.

Un transporteur de plus, une classe de plus
interface Transporteur
{
    public function fraisDePort(Colis $colis): float;
}

class Colissimo implements Transporteur
{
    public function fraisDePort(Colis $colis): float
    {
        return 4.90 + $colis->poids * 0.5;
    }
}

class Chronopost implements Transporteur
{
    public function fraisDePort(Colis $colis): float
    {
        return 12.00 + $colis->poids * 0.8;
    }
}

// Ajouter Mondial Relay, c'est écrire une nouvelle classe,
// sans toucher à Colissimo ni à Chronopost

C'est exactement le pattern Strategy vu dans le chapitre précédent. Les patterns sont des façons concrètes de respecter ces principes, pas les principes eux-mêmes.

L, la substitution de Liskov

Quand une classe hérite d'une autre, l'enfant doit pouvoir remplacer son parent partout, sans mauvaise surprise pour le reste du code. Si la mère promet quelque chose, chaque enfant doit tenir cette promesse.

Reprenons les moyens de paiement du chapitre sur la programmation objet. La classe mère promet que tout moyen de paiement sait débiter et rembourser. Arrive le paiement à la livraison, qui ne peut rien rembourser automatiquement. Le développeur pressé en fait quand même un enfant de MoyenDePaiement, et écrit une méthode rembourser qui déclenche une erreur.

Sur le moment, tout fonctionne. Mais le jour où le service client lance un remboursement groupé après une rupture de stock, le programme parcourt tous les moyens de paiement en faisant confiance à la promesse de la mère, et s'arrête net au premier paiement à la livraison.

Un enfant qui répond "je ne sais pas faire" à la place de son parent est le signal classique d'une violation de ce principe. Le problème ne vient pas de l'enfant, mais de la hiérarchie elle-même, qui promettait trop. C'est tout le sujet de la section quand l'héritage se retourne contre toi.

I, la ségrégation des interfaces

Mieux vaut plusieurs petits contrats précis qu'un seul contrat géant. Une classe ne devrait jamais être obligée de promettre des choses dont elle n'a pas l'usage.

Imagine un catalogue qui vend des livres papier, des ebooks et des abonnements à un magazine. Un seul contrat Produit impose à tout le monde de savoir être expédié, téléchargé et renouvelé chaque mois. L'ebook se retrouve à écrire une méthode d'expédition qui ne fait rien, et le livre papier une méthode de téléchargement tout aussi vide.

Des petits contrats qu'on combine
interface Expediable
{
    public function expedier(Adresse $adresse): void;
}

interface Telechargeable
{
    public function lienDeTelechargement(): string;
}

interface Renouvelable
{
    public function renouveler(): void;
}

class LivrePapier implements Expediable
{
    // ...
}

class Ebook implements Telechargeable
{
    // ...
}

class AbonnementMagazine implements Expediable, Renouvelable
{
    // ...
}

Chaque produit ne promet que ce qu'il sait faire. Et le jour où la façon d'expédier change, les ebooks ne sont même pas concernés.

D, l'inversion des dépendances

Le coeur de l'application, ses règles métier, ne doit pas dépendre des détails techniques. Ce sont les détails qui viennent s'y brancher.

Sans ce principe, la classe qui valide une commande appelle directement le service de tel prestataire d'emails. Les règles métier les plus précieuses se retrouvent collées à un outil qu'on changera forcément un jour.

Avec ce principe, la validation de commande dit simplement "j'ai besoin de quelque chose qui sait envoyer un email", sous la forme d'une interface. C'est ensuite une petite brique à part, un Adapter, qui fait le lien avec le prestataire du moment.

Qui dépend de qui
Sans inversion

  ValidationDeCommande ─────────▶ PrestataireDEmails
  (règle métier)                  (détail technique)

Avec inversion

  ValidationDeCommande ─────────▶ EnvoyeurDEmails
  (règle métier)                  (contrat défini par le métier)
                                         ▲
                                         │ respecte le contrat
                                         │
                                  AdapterPrestataire
                                  (détail technique)

Changer de prestataire revient alors à écrire un nouvel Adapter, sans toucher à la validation de commande. C'est aussi ce qui permet, pendant les tests, de brancher un faux envoyeur qui n'envoie rien à personne.

Le revers de la médaille

Comme les design patterns, SOLID a ses excès. Appliqué à la lettre, chaque principe pousse à découper davantage, et un code peut finir éclaté en quarante petites classes et autant d'interfaces, où plus personne ne retrouve le chemin d'une simple fonctionnalité.

Ces principes sont des repères, pas des lois. Les appliquer "au cas où", sur un code qui n'évoluera jamais, coûte du temps et de la lisibilité pour un bénéfice nul. Ils prennent tout leur sens là où le métier change souvent.

Pourquoi c'est important pour toi ?

Tu n'écriras sans doute jamais une interface. Mais SOLID explique une bonne partie des écarts d'estimation que tu entends. Un ajout prévu par la conception se chiffre en heures. Le même ajout dans un code où tout est mélangé se chiffre en jours, parce qu'il faut d'abord comprendre, puis démêler, puis tout retester.

  • Quand tu prépares une évolution : demande combien d'endroits du code elle touche. Si la réponse est "un peu partout", c'est souvent une responsabilité mal découpée.
  • Quand on envisage de changer de prestataire : demande si l'outil actuel est isolé derrière une brique à part. La réponse change complètement le coût du projet.
  • Quand l'équipe demande du temps pour réorganiser le code : c'est souvent pour remettre ces principes d'aplomb sur une partie qui freine toutes les demandes. Ce temps se récupère sur les évolutions suivantes, c'est tout le sujet de la dette technique.

Un code SOLID ne va pas plus vite le premier jour. Il va plus vite le centième, quand le métier a changé dix fois d'avis et que chaque nouvelle demande ne touche qu'une petite partie du code.

PrécédentLes design patterns Tous les chapitres SuivantLes erreurs