GevoxxInsights All articles
Software engineering · Architecture

SOLID : les cinq principes qui rendent le code orienté objet maintenable

SRP, OCP, LSP, ISP, DIP : ce que chaque lettre signifie vraiment, avec des exemples de code avant/après et les pièges à éviter.

SOLID : les cinq principes qui rendent le code orienté objet maintenable

SOLID est un acronyme regroupant cinq principes de conception orientée objet formulés en grande partie par Robert C. Martin (« Uncle Bob »). Leur but n'est pas d'être des règles rigides mais des heuristiques qui rendent le code plus facile à comprendre, à modifier et à tester. Un code qui les respecte résiste mieux au temps : on peut y ajouter une fonctionnalité sans tout casser. Décortiquons-les, lettre par lettre, avec du code.

S — Single Responsibility Principle (responsabilité unique)

Une classe ne devrait avoir qu'une seule raison de changer. Autrement dit, elle ne rend de comptes qu'à un seul « acteur » (une seule préoccupation métier). Une classe qui calcule une paie, la formate en PDF et l'envoie par e-mail a trois raisons de changer — trois responsabilités mêlées.

// ❌ Trop de responsabilités
class Facture {
    double total() { /* calcul */ return 0; }
    String versPdf() { /* mise en forme */ return ""; }
    void envoyer() { /* SMTP */ }
}

// ✅ Séparées
class Facture { double total() { return 0; } }
class FacturePdf { String rendre(Facture f) { return ""; } }
class EnvoiEmail { void envoyer(String contenu) { } }

Le bénéfice : changer la mise en forme du PDF n'oblige plus à toucher au calcul du total, et chaque classe se teste isolément.

O — Open/Closed Principle (ouvert/fermé)

Une entité doit être ouverte à l'extension mais fermée à la modification. On doit pouvoir ajouter un comportement sans modifier le code existant — typiquement via le polymorphisme. Le symptôme d'une violation : un switch ou une cascade de if sur un type qui grossit à chaque nouveau cas.

// ❌ Il faut modifier la méthode à chaque nouveau moyen de paiement
double frais(String type) {
    if (type.equals("CB")) return 0.015;
    if (type.equals("PAYPAL")) return 0.025;
    // ... et ainsi de suite
    return 0;
}

// ✅ On ajoute une classe, on ne modifie rien
interface MoyenPaiement { double frais(); }
class CarteBancaire implements MoyenPaiement { public double frais() { return 0.015; } }
class Paypal implements MoyenPaiement { public double frais() { return 0.025; } }

L — Liskov Substitution Principle (substitution de Liskov)

Un objet d'une classe fille doit pouvoir remplacer un objet de sa classe mère sans casser le programme. Le contre-exemple classique : Carre extends Rectangle. Un carré force largeur = hauteur, ce qui viole les attentes du code qui manipule un rectangle (« je change la largeur, la hauteur ne doit pas bouger »). Si une sous-classe doit lever une exception ou ne rien faire pour respecter son interface, c'est un signal d'alerte : l'héritage est mal placé.

La question à se poser : « toute méthode qui accepte le parent fonctionne-t-elle correctement avec l'enfant ? » Si non, l'héritage ment.

I — Interface Segregation Principle (ségrégation des interfaces)

Mieux vaut plusieurs interfaces spécifiques qu'une seule interface géante. Une classe ne devrait pas être forcée d'implémenter des méthodes dont elle n'a pas besoin. Une interface Machine avec imprimer(), scanner(), faxer() oblige une imprimante simple à implémenter faxer() (souvent avec un corps vide ou une exception). Découpez : Imprimante, Scanner, Fax. Chaque classe n'implémente que ce qu'elle sait faire.

D — Dependency Inversion Principle (inversion des dépendances)

Les modules de haut niveau ne doivent pas dépendre des modules de bas niveau : tous deux dépendent d'abstractions. Concrètement, votre logique métier ne devrait pas dépendre directement de MySqlUserRepository, mais d'une interface UserRepository ; l'implémentation concrète est injectée. C'est le fondement de l'injection de dépendances (Spring, entre autres) et la clé de la testabilité : en test, on injecte un faux dépôt en mémoire.

class ServiceUtilisateur {
    private final UserRepository repo; // abstraction
    ServiceUtilisateur(UserRepository repo) { this.repo = repo; } // injecté
}

Le mot de la fin : des principes, pas des dogmes

SOLID guide vers un code découplé et testable, mais appliqué à l'excès il produit une avalanche d'interfaces et d'abstractions inutiles (« over-engineering »). Le bon réflexe : reconnaître les symptômes (une classe qui change tout le temps, un switch qui grossit, une sous-classe qui triche) et appliquer le principe concerné quand il résout une douleur réelle. La simplicité reste la vertu suprême.

Sources et références