Les principes SOLID sont cinq règles fondamentales de la conception orientée objet, formulées par Robert C. Martin. Ils vous aident à écrire du code plus maintenable, testable et résilient au changement. Ce guide présente chaque principe avec des exemples Python, des comparaisons mauvais-vs-bon et des applications pratiques.
Tableau récapitulatif
| Principe | En bref | Section |
|---|---|---|
| Single Responsibility | Une classe, une responsabilité | s-2 |
| Open/Closed | Ouvert à l’extension, fermé à la modification | s-3 |
| Liskov Substitution | Les sous-types doivent être substituables | s-4 |
| Interface Segregation | Interfaces petites et ciblées | s-5 |
| Dependency Inversion | Dépendre des abstractions, pas des implémentations | s-6 |
Pourquoi les principes SOLID sont-ils nécessaires ?
Sans principes de conception clairs, le code tend à devenir un « plat de spaghettis » : des classes qui font tout, des dépendances enchevêtrées, et des modifications qui provoquent des régressions imprévisibles. Les principes SOLID offrent des garde-fous pour éviter ces pièges.
Les principes SOLID ne sont pas des règles absolues. Ils guident vos décisions de conception ; appliquez-les avec discernement selon le contexte de votre projet.
Sur-appliquer SOLID sur un petit script de 50 lignes ajoute de la complexité inutile. Réservez ces principes aux modules qui évoluent et aux équipes.
S — Responsabilité Unique (SRP)
Une classe ne doit avoir qu’une seule raison de changer. Si elle gère la récupération des données, le formatage et l’envoi d’emails, toute modification dans l’un de ces domaines la touche.
Mauvais exemple
class UserReport:
def __init__(self, user):
self.user = user
def generate_report(self):
# Logique métier
data = self._fetch_data()
# Formatage
html = self._to_html(data)
# Envoi email
self._send_email(html)
return html
def _fetch_data(self): ...
def _to_html(self, data): ...
def _send_email(self, html): ...
Bon exemple
class UserDataFetcher:
def fetch(self, user): ...
class ReportFormatter:
def to_html(self, data): ...
class EmailSender:
def send(self, to, html): ...
class ReportService:
def __init__(self, fetcher, formatter, sender):
self.fetcher = fetcher
self.formatter = formatter
self.sender = sender
def generate_and_send(self, user):
data = self.fetcher.fetch(user)
html = self.formatter.to_html(data)
self.sender.send(user.email, html)
Chaque classe a une responsabilité claire. Les tests sont plus simples, et modifier l’envoi d’emails n’affecte pas le formatage.
Voir aussi : Gestion des erreurs en Python pour des patterns complémentaires.
O — Ouvert/Fermé (OCP)
Les entités logicielles doivent être ouvertes à l’extension mais fermées à la modification. Ajouter un nouveau type de forme ne doit pas vous obliger à modifier une fonction existante.
Mauvais exemple
def calculate_area(shape):
if shape[\u0022type\u0022] == \u0022circle\u0022:
return 3.14 * shape[\u0022radius\u0022] ** 2
elif shape[\u0022type\u0022] == \u0022rectangle\u0022:
return shape[\u0022width\u0022] * shape[\u0022height\u0022]
elif shape[\u0022type\u0022] == \u0022triangle\u0022:
return 0.5 * shape[\u0022base\u0022] * shape[\u0022height\u0022]
# Chaque nouveau type = modifier cette fonction
Bon exemple
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
pass
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
return 3.14 * 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
# Nouveau type = nouvelle classe, pas de modification du code existant
Utilisez des abstractions (ABC, interfaces) pour définir des contrats. Les nouvelles fonctionnalités s’ajoutent par de nouvelles implémentations.
L — Substitution de Liskov (LSP)
Les objets d’une sous-classe doivent pouvoir remplacer les objets de la classe parente sans casser le programme. Si le code attend un Rectangle et reçoit un Square, il doit continuer à fonctionner correctement.
Mauvais exemple
class Rectangle:
def __init__(self, w, h):
self.width, self.height = w, h
class Square(Rectangle):
def __init__(self, side):
super().__init__(side, side)
def set_width(self, w):
self.width = self.height = w # Casse le contrat de Rectangle !
Bon exemple
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
pass
class Rectangle(Shape):
def __init__(self, w, h):
self.width, self.height = w, h
def area(self) -> float:
return self.width * self.height
class Square(Shape):
def __init__(self, side):
self.side = side
def area(self) -> float:
return self.side ** 2
# Les deux respectent le contrat Shape, substituables dans tout code qui attend une Shape
Faire hériter Square de Rectangle semble logique, mais Square a des invariants différents (largeur = hauteur). Préférez une hiérarchie commune (Shape) plutôt qu’une hiérarchie carrée-rectangle.
I — Ségrégation d’Interfaces (ISP)
Les clients ne doivent pas dépendre d’interfaces qu’ils n’utilisent pas. Une interface « Worker » avec work(), eat() et sleep() force un Robot à implémenter eat() et sleep() inutilement.
Mauvais exemple
class Worker(ABC):
@abstractmethod
def work(self): ...
@abstractmethod
def eat(self): ...
@abstractmethod
def sleep(self): ...
class Robot(Worker):
def work(self): ...
def eat(self): raise NotImplementedError # Robot ne mange pas !
def sleep(self): raise NotImplementedError
Bon exemple
class Workable(ABC):
@abstractmethod
def work(self): ...
class Eatable(ABC):
@abstractmethod
def eat(self): ...
class Human(Workable, Eatable):
def work(self): ...
def eat(self): ...
class Robot(Workable):
def work(self): ...
# Pas besoin d\u0027implémenter eat()
Des interfaces petites et ciblées (Workable, Eatable) permettent à chaque classe d’implémenter uniquement ce dont elle a besoin.
D — Inversion de Dépendances (DIP)
Dépendre des abstractions, pas des implémentations concrètes. Une classe OrderService ne doit pas instancier directement EmailNotifier ; elle doit recevoir une abstraction (Notifier) en paramètre.
Mauvais exemple
class EmailNotifier:
def send(self, msg): ...
class OrderService:
def __init__(self):
self.notifier = EmailNotifier() # Dépendance concrète
def place_order(self, order):
# ...
self.notifier.send(\u0022Order placed\u0022)
Bon exemple
from abc import ABC, abstractmethod
class Notifier(ABC):
@abstractmethod
def send(self, msg: str): ...
class OrderService:
def __init__(self, notifier: Notifier):
self.notifier = notifier # Dépendance abstraite
def place_order(self, order):
self.notifier.send(\u0022Order placed\u0022)
# En production : OrderService(EmailNotifier())
# En test : OrderService(MockNotifier())
L’injection de dépendances facilite les tests (mock) et permet de changer d’implémentation (SMS, webhook) sans modifier OrderService.
Avant / Après : impact sur le code
Avant SOLID : une classe « God » de 500 lignes, des tests difficiles, des régressions à chaque changement. Après SOLID : des modules cohérents, des tests unitaires ciblés, des évolutions par extension plutôt que par modification.
Consultez Python en 5 minutes pour les bases de la programmation orientée objet en Python.
Mise en pratique
1. Refactorisation progressive : identifiez les classes les plus « god » et commencez par extraire une responsabilité.
2. Tests : si une classe est difficile à tester, c’est souvent un signe de violation de SRP ou DIP.
3. Revues de code : utilisez SOLID comme checklist lors des revues.
Appliquez d’abord SRP et DIP ; ils apportent le plus de bénéfices immédiats. OCP et LSP deviennent plus naturels une fois la structure clarifiée.
Ordre d’apprentissage recommandé
1. SRP — le plus intuitif, à appliquer en premier.
2. DIP — crucial pour la testabilité et la flexibilité.
3. OCP — extension vs modification.
4. ISP — interfaces petites.
5. LSP — plus subtil, souvent compris après avoir vu les autres.
Idées reçues sur SOLID
« SOLID = plus de classes » — Oui, mais des classes plus petites et focalisées. La complexité est déplacée, pas supprimée.
« SOLID = over-engineering » — Pas si vous l’appliquez avec mesure. Un script de 10 lignes n’a pas besoin d’ABC.
« SOLID ne s’applique qu’à Java/C# » — Faux. Python, avec ABC et le typage, supporte très bien ces principes.
Prochaines étapes
Après avoir maîtrisé SOLID, explorez : Patterns de génération de mots de passe, les design patterns (Strategy, Factory, Observer) et les principes CLEAN (Clean Architecture).
FAQ
Les principes SOLID s’appliquent-ils au code Python ?
Oui. Python supporte l’ABC (Abstract Base Classes), l’héritage, les interfaces implicites. SOLID est indépendant du langage.
Faut-il toujours respecter les cinq principes ?
Ils sont des guides, pas des lois. SRP et DIP sont les plus universels ; les autres s’appliquent selon le contexte.
Comment détecter les violations de SOLID ?
Classes qui changent souvent pour des raisons différentes (SRP), code difficile à tester (DIP), hiérarchies d’héritage contre-intuitives (LSP).
Résumé
Les principes SOLID sont cinq règles de conception orientée objet : Single Responsibility (une classe, une responsabilité), Open/Closed (extension sans modification), Liskov Substitution (sous-types substituables), Interface Segregation (interfaces ciblées), Dependency Inversion (dépendre des abstractions). Appliqués avec discernement, ils produisent un code plus maintenable et testable.

Laisser un commentaire