Les principes SOLID
Les principes
SOLID
Cinq principes fondamentaux pour concevoir un code orienté objet robuste, maintenable et évolutif.
En 2000, Robert C. Martin — surnommé Uncle Bob — formalisait cinq principes de conception orientée objet qui allaient devenir l’un des piliers de l’ingénierie logicielle moderne. Regroupés sous l’acronyme SOLID, ces principes ne sont pas des règles arbitraires : ils sont la réponse directe aux pathologies les plus courantes du code mal conçu — rigidité, fragilité, immobilité, viscosité.
Un code qui viole SOLID est un code qui résiste au changement. Il se brise quand on le modifie, cascade ses effets dans des endroits inattendus, et devient progressivement impossible à tester ou à réutiliser. SOLID est l’antidote.
— Robert C. Martin, Clean Code
Ces principes s’appliquent principalement à la programmation orientée objet, mais leur esprit transcende les paradigmes. Que vous écriviez en PHP, Python, Java, TypeScript ou C#, SOLID vous offrira un cadre de réflexion pour des décisions d’architecture saines.
| Lettre | Principe | En une phrase |
|---|---|---|
| S | Single Responsibility | Une classe = une raison de changer |
| O | Open / Closed | Ouvert à l’extension, fermé à la modification |
| L | Liskov Substitution | Une sous-classe doit se substituer à sa classe parente |
| I | Interface Segregation | Mieux vaut plusieurs petites interfaces qu’une grande |
| D | Dependency Inversion | Dépendre des abstractions, pas des implémentations |
Définition
Le principe de responsabilité unique stipule qu’une classe — ou plus largement, un module, une fonction — doit être responsable d’une seule chose, et ne doit changer que pour une seule raison. Cette « raison de changer » est généralement liée à un acteur spécifique : un utilisateur, un département, un système externe dont les exigences peuvent évoluer.
L’erreur classique est de confondre SRP avec « une classe ne fait qu’une chose ». Une classe peut avoir plusieurs méthodes — l’important est que toutes ces méthodes servent la même responsabilité cohérente. Un UserRepository peut avoir findById(), findByEmail(), save() — toutes ces méthodes servent la même responsabilité : la persistence des utilisateurs.
Pourquoi c’est important
Une classe qui fait trop de choses devient un point de couplage. Quand la logique métier, la persistence, la validation et la notification cohabitent dans la même classe, toute modification touche potentiellement des fonctionnalités sans rapport. Les tests deviennent difficiles, les bugs se propagent, la réutilisation devient impossible.
Anti-pattern : une classe
User qui gère à la fois les données utilisateur, l’envoi d’emails ET la génération de rapports PDF. Ces trois responsabilités appartiennent à trois acteurs différents (équipe métier, équipe communication, équipe reporting) et changeront pour des raisons différentes.
Exemple — Violation du SRP
// Une classe qui fait TROP de choses
class User
{
private string $name;
private string $email;
// Responsabilité 1 : données utilisateur ✓
public function getName(): string { return $this->name; }
public function getEmail(): string { return $this->email; }
// Responsabilité 2 : persistence en base — VIOLATION
public function save(): void
{
$pdo = new PDO('mysql:...');
$pdo->query("INSERT INTO users ...");
}
// Responsabilité 3 : envoi d'email — VIOLATION
public function sendWelcomeEmail(): void
{
mail($this->email, 'Bienvenue', '...');
}
// Responsabilité 4 : rapport PDF — VIOLATION
public function generateReport(): string
{
return "<pdf>" . $this->name . "</pdf>";
}
}
Exemple — Respect du SRP
// Responsabilité 1 : entité utilisateur (données uniquement)
class User
{
public function __construct(
private readonly string $name,
private readonly string $email
) {}
public function getName(): string { return $this->name; }
public function getEmail(): string { return $this->email; }
}
// Responsabilité 2 : persistence
class UserRepository
{
public function save(User $user): void
{
// Toute la logique d'accès à la base de données ici
}
public function findByEmail(string $email): ?User
{
// ...
}
}
// Responsabilité 3 : notification
class UserMailer
{
public function sendWelcome(User $user): void
{
// Envoi d'email de bienvenue
}
}
// Responsabilité 4 : reporting
class UserReportGenerator
{
public function generate(User $user): string
{
// Génération du rapport PDF
}
}
Bénéfices concrets : chaque classe peut maintenant être testée indépendamment. Changer l’ORM ne touche que
UserRepository. Changer le fournisseur d’email ne touche que UserMailer. Zéro effet de bord.
Définition
Formulé par Bertrand Meyer en 1988 puis repris par Robert Martin, l’OCP signifie qu’une classe doit pouvoir être étendue sans être modifiée. En pratique : lorsqu’une nouvelle fonctionnalité est requise, on ajoute du code — on n’en modifie pas. La classe existante reste stable, testée, fiable.
Ce principe s’applique via l’héritage, la composition et les interfaces/abstractions. Le code qui dépend de l’abstraction n’a pas à changer quand on ajoute une nouvelle implémentation.
Le problème du switch géant
La violation la plus classique de l’OCP est le switch ou la chaîne de if/elseif qui doit être étendue à chaque nouveau type. Chaque ajout oblige à modifier la classe existante — risque de régression, obligation de re-tester tout.
class DiscountCalculator
{
public function calculate(string $customerType, float $price): float
{
// Chaque nouveau type de client = modifier cette méthode
switch ($customerType) {
case 'regular':
return $price;
case 'vip':
return $price * 0.85;
case 'employee':
return $price * 0.50;
// Demain : 'partner', 'reseller', 'influencer'... ?
default:
return $price;
}
}
}
// Interface fermée à la modification
interface DiscountStrategy
{
public function apply(float $price): float;
}
// Implémentations ouvertes à l'extension
class RegularDiscount implements DiscountStrategy
{
public function apply(float $price): float { return $price; }
}
class VipDiscount implements DiscountStrategy
{
public function apply(float $price): float { return $price * 0.85; }
}
class EmployeeDiscount implements DiscountStrategy
{
public function apply(float $price): float { return $price * 0.50; }
}
// Demain : nouveau type → AUCUNE modification existante
class PartnerDiscount implements DiscountStrategy
{
public function apply(float $price): float { return $price * 0.70; }
}
// Le calculateur ne change jamais
class DiscountCalculator
{
public function __construct(
private readonly DiscountStrategy $strategy
) {}
public function calculate(float $price): float
{
return $this->strategy->apply($price);
}
}
Ajouter un nouveau type de remise ne touche aucune ligne de code existante. On crée une nouvelle classe, on l’injecte — et le reste du système l’accepte sans modification. C’est le pattern Strategy, l’une des implémentations les plus courantes de l’OCP.
En Python
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
...
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
return 3.14159 * self.radius ** 2
class Rectangle(Shape):
def __init__(self, width: float, height: float):
self.width, self.height = width, height
def area(self) -> float:
return self.width * self.height
# Ajout de Triangle → aucune modification de Circle ou Rectangle
class Triangle(Shape):
def __init__(self, base: float, height: float):
self.base, self.height = base, height
def area(self) -> float:
return 0.5 * self.base * self.height
def total_area(shapes: list[Shape]) -> float:
return sum(s.area() for s in shapes)
Définition
Énoncé par Barbara Liskov en 1987, ce principe précise les conditions d’un héritage correct. Si B hérite de A, tout code qui utilise A doit pouvoir utiliser B sans rien savoir de B et sans que le comportement attendu ne soit violé.
En d’autres termes : une sous-classe ne doit pas renforcer les préconditions, affaiblir les postconditions, ni lancer des exceptions inattendues. Si la sous-classe oblige l’appelant à « faire attention », le principe est violé.
L’exemple classique : Rectangle et Carré
Intuitivement, un carré est un rectangle (en mathématiques). En POO, cet héritage est piégeux — et c’est l’exemple canonique du LSP violé.
class Rectangle
{
protected int $width;
protected int $height;
public function setWidth(int $w): void { $this->width = $w; }
public function setHeight(int $h): void { $this->height = $h; }
public function area(): int { return $this->width * $this->height; }
}
class Square extends Rectangle
{
// Le carré force les deux dimensions à être égales
public function setWidth(int $w): void
{
$this->width = $w;
$this->height = $w; // ← surprise !
}
public function setHeight(int $h): void
{
$this->width = $h; // ← surprise !
$this->height = $h;
}
}
// Comportement qui "marchait" avec Rectangle mais échoue avec Square
function resizeAndCheck(Rectangle $r): void
{
$r->setWidth(4);
$r->setHeight(5);
// Attendu : 20 | Avec Square : 25 → VIOLATION
assert($r->area() === 20);
}
// Abstraction commune sans héritage problématique
interface Shape
{
public function area(): float;
}
class Rectangle implements Shape
{
public function __construct(
private readonly float $width,
private readonly float $height
) {}
public function area(): float
{
return $this->width * $this->height;
}
}
class Square implements Shape
{
public function __construct(
private readonly float $side
) {}
public function area(): float
{
return $this->side ** 2;
}
}
// Fonctionne correctement avec les deux
function printArea(Shape $shape): void
{
echo "Aire : " . $shape->area();
}
Règles pratiques pour respecter le LSP
Préconditions : une méthode de sous-classe ne peut pas exiger plus que la méthode parente.
Postconditions : une méthode de sous-classe ne peut pas garantir moins que la méthode parente.
Invariants : les propriétés fondamentales de la classe parente doivent être préservées.
Exceptions : une sous-classe ne peut pas lever de nouveaux types d’exceptions non déclarés dans le parent.
Le LSP est le principe le plus subtil à diagnostiquer. Un signe révélateur : si vous avez des
instanceof dans votre code pour « savoir quel type réel on manipule », c’est presque toujours une violation du LSP.
Définition
L’ISP préconise de préférer plusieurs interfaces petites et spécialisées à une seule interface large et généraliste — parfois appelée « fat interface ». L’objectif est d’éviter qu’une classe soit forcée d’implémenter des méthodes qu’elle n’utilise pas, créant des dépendances parasites.
Quand une interface est trop large, toute modification la touchant propage des impacts à toutes les classes qui l’implémentent — même celles qui n’utilisent pas la méthode modifiée.
L’exemple de l’imprimante multifontions
// Interface trop large : pas toutes les imprimantes scannent ou faxent
interface Machine
{
public function print(Document $doc): void;
public function scan(Document $doc): void;
public function fax(Document $doc): void;
public function staple(Document $doc): void;
}
// Une simple imprimante est forcée d'implémenter scan, fax, staple
class SimplePrinter implements Machine
{
public function print(Document $doc): void { /* implémentation réelle */ }
// Ces méthodes ne font rien — ou pire, lancent une exception
public function scan(Document $doc): void
{
throw new BadMethodCallException('Non supporté');
}
public function fax(Document $doc): void
{
throw new BadMethodCallException('Non supporté');
}
public function staple(Document $doc): void
{
throw new BadMethodCallException('Non supporté');
}
}
// Interfaces séparées, chacune avec une seule responsabilité
interface Printable
{
public function print(Document $doc): void;
}
interface Scannable
{
public function scan(Document $doc): void;
}
interface Faxable
{
public function fax(Document $doc): void;
}
// Simple imprimante : implémente seulement ce qu'elle sait faire
class SimplePrinter implements Printable
{
public function print(Document $doc): void
{
// Impression réelle
}
}
// Multifonction : combine les interfaces dont elle a besoin
class AllInOnePrinter implements Printable, Scannable, Faxable
{
public function print(Document $doc): void { /* ... */ }
public function scan(Document $doc): void { /* ... */ }
public function fax(Document $doc): void { /* ... */ }
}
// Le code client ne dépend que de ce dont il a besoin
function printReport(Printable $printer, Document $doc): void
{
$printer->print($doc);
}
ISP et frameworks modernes
L’ISP est particulièrement visible dans les frameworks modernes. Laravel, par exemple, propose des dizaines de contrats (contracts) comme Illuminate\Contracts\Auth\Authenticatable, Illuminate\Contracts\Queue\ShouldQueue — chacun ne demande que les méthodes strictement nécessaires à son rôle.
Règle heuristique : si une interface a plus de 5-7 méthodes, posez-vous la question de sa cohérence. Des méthodes qui « vont ensemble » forment une interface. Des méthodes qui « servent des besoins différents » devraient être séparées. L’ISP est aussi une question de couplage à la compilation : une petite interface minimise le nombre de modules qui doivent recompiler lorsqu’elle change.
Définition
Le DIP est souvent le moins intuitif des cinq principes, mais c’est celui qui a le plus d’impact sur l’architecture globale. Il dit en substance : ne programmez pas contre des implémentations concrètes, programmez contre des interfaces. Les détails d’implémentation (bases de données, services externes, librairies) doivent dépendre des abstractions définies par la logique métier — et non l’inverse.
C’est le fondement de l’injection de dépendances (DI) et des conteneurs IoC (Inversion of Control) utilisés dans Symfony, Laravel, Spring, .NET Core, etc.
Le problème du couplage fort
// Module de haut niveau (logique métier)
class OrderService
{
private MySQLOrderRepository $repo;
private SmtpMailer $mailer;
private StripePaymentGateway $gateway;
public function __construct()
{
// Dépendances créées en dur — impossible à tester ou remplacer
$this->repo = new MySQLOrderRepository();
$this->mailer = new SmtpMailer('smtp.mailtrap.io');
$this->gateway = new StripePaymentGateway('sk_live_...');
}
public function placeOrder(Order $order): void
{
$this->gateway->charge($order);
$this->repo->save($order);
$this->mailer->send($order);
}
// Problèmes :
// • Changer MySQL → PostgreSQL = modifier OrderService
// • Changer Stripe → PayPal = modifier OrderService
// • Tests unitaires impossibles (appels réels à SMTP, Stripe, MySQL)
}
// Abstractions définies par la logique métier
interface OrderRepositoryInterface
{
public function save(Order $order): void;
public function findById(int $id): ?Order;
}
interface MailerInterface
{
public function send(string $to, string $subject, string $body): void;
}
interface PaymentGatewayInterface
{
public function charge(Order $order): PaymentResult;
}
// Module de haut niveau — dépend uniquement des interfaces
class OrderService
{
public function __construct(
private readonly OrderRepositoryInterface $repository,
private readonly MailerInterface $mailer,
private readonly PaymentGatewayInterface $gateway
) {}
public function placeOrder(Order $order): void
{
$result = $this->gateway->charge($order);
$this->repository->save($order);
$this->mailer->send(
$order->getCustomerEmail(),
'Commande confirmée',
'...'
);
}
}
// Implémentations concrètes (modules de bas niveau)
class MySQLOrderRepository implements OrderRepositoryInterface
{
public function save(Order $order): void { /* ... */ }
public function findById(int $id): ?Order { /* ... */ }
}
class StripePaymentGateway implements PaymentGatewayInterface
{
public function charge(Order $order): PaymentResult { /* ... */ }
}
// Pour les tests : implémentations mock — SANS modifier OrderService
class InMemoryOrderRepository implements OrderRepositoryInterface
{
private array $store = [];
public function save(Order $order): void { $this->store[] = $order; }
public function findById(int $id): ?Order { return $this->store[$id] ?? null; }
}
Injection de dépendances et conteneurs IoC
En pratique, l’injection des dépendances est gérée par un conteneur IoC qui lit la configuration et instancie les bonnes implémentations automatiquement. Dans Symfony, par exemple :
# Le conteneur lit ce fichier et injecte les bonnes implémentations
services:
App\Repository\OrderRepositoryInterface:
alias: App\Repository\MySQLOrderRepository
# En environnement de test, on utilise une autre implémentation
# App\Repository\OrderRepositoryInterface:
# alias: App\Repository\InMemoryOrderRepository
Bénéfices concrets du DIP : tests unitaires faciles (on injecte des mocks), changement de base de données sans toucher à la logique métier, changement de provider email sans modification, possibilité de tester
OrderService complètement isolé de toute dépendance externe.
DIP vs Dependency Injection
À ne pas confondre : le DIP est un principe (dépendre des abstractions), l’injection de dépendances est un pattern (mécanisme pour satisfaire ces dépendances de l’extérieur). L’un n’implique pas forcément l’autre, mais ils sont complémentaires dans 99% des cas.
Récapitulatif — Les 5 principes SOLID
SOLID n’est pas une checklist à cocher mécaniquement. C’est un cadre de réflexion pour poser les bonnes questions au bon moment. Appliquer SOLID systématiquement à des petits scripts ou des prototypes peut être contre-productif. Mais dans tout système destiné à croître et à être maintenu par une équipe, ces principes font la différence entre un code qui résiste au temps et un code qui s’effondre à la première extension.
Photo : Daniil Komov sur Pexels.
Pour aller plus loin
Liens affiliés Amazon : en tant que Partenaire Amazon, ce site perçoit une commission sur les achats éligibles réalisés via ces liens, sans coût supplémentaire pour vous.
Coder proprement : Le manuel de l’artisanat logiciel agile, Robert C. Martin
Rédigé par l’auteur qui a popularisé les principes SOLID, cet ouvrage incontournable détaille comment les appliquer concrètement au quotidien pour produire un code lisible, testable et facile à maintenir.
Clean Architecture, Robert C. Martin
Ce livre va plus loin en appliquant les principes SOLID à la structure globale de l’application, montrant comment découper les modules et gérer leurs dépendances pour bâtir des architectures durables.
Design Patterns : Catalogue de modèles de conception réutilisables, Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides
Ouvrage de référence sur les patrons de conception orientée objet, il illustre parfaitement la mise en pratique des principes SOLID pour résoudre des problématiques d’architecture récurrentes.