Principes SOLID¶
Cinq principes pour écrire du code orientée objet qui reste maintenable quand le projet grandit.
Le problème que SOLID résout¶
Du code qui ne respecte pas ces principes devient rapidement :
- Rigide : modifier une fonctionnalité casse autre chose
- Fragile : les effets de bord sont impredictibles
- Immobile : rien n'est réutilisable sans embarquer des dépendances inutiles
SOLID n'est pas une checklist a cocher — c'est un système de signaux d'alarme. Quand votre code vous fait souffrir, un principe est probablement viole.
S — Single Responsibility Principle¶
Une classe n'a qu'une seule raison de changer.
Une classe qui géré à la fois la logique métier et la persistance va changer quand la base de données change ET quand la règle métier change. Ces deux axes d'évolution ne doivent pas se trouver dans le même fichier.
class Order:
def __init__(self, items):
self.items = items
def calculate_total(self):
return sum(item.price for item in self.items)
def save_to_db(self, connection):
# logique metier + persistence = mauvais melange
connection.execute(
"INSERT INTO orders (total) VALUES (?)",
(self.calculate_total(),)
)
def send_confirmation_email(self, smtp_client):
# encore une responsabilite de plus
smtp_client.send(f"Votre commande : {self.calculate_total()} EUR")
class Order:
def __init__(self, items):
self.items = items
def calculate_total(self):
return sum(item.price for item in self.items)
class OrderRepository:
def __init__(self, connection):
self.connection = connection
def save(self, order: Order):
self.connection.execute(
"INSERT INTO orders (total) VALUES (?)",
(order.calculate_total(),)
)
class OrderNotifier:
def __init__(self, smtp_client):
self.smtp_client = smtp_client
def confirm(self, order: Order):
self.smtp_client.send(f"Votre commande : {order.calculate_total()} EUR")
O — Open/Closed Principle¶
Ouvert à l'extension, ferme à la modification.
Ajouter un comportement ne doit pas necessiter de modifier le code existant. On etend, on ne touche pas.
from abc import ABC, abstractmethod
class ReportExporter(ABC):
@abstractmethod
def export(self, report) -> bytes:
pass
class PdfExporter(ReportExporter):
def export(self, report) -> bytes:
# logique PDF isolee
pass
class CsvExporter(ReportExporter):
def export(self, report) -> bytes:
# logique CSV isolee
pass
# ajouter XlsxExporter ne touche a rien d'existant
L — Liskov Substitution Principle¶
Un sous-type doit pouvoir remplacer son type parent sans casser le programme.
Si une fonction accepte un Animal, elle doit fonctionner avec n'importe quelle sous-classe d'Animal sans surprise.
class Rectangle:
def set_width(self, w): self.width = w
def set_height(self, h): self.height = h
def area(self): return self.width * self.height
class Square(Rectangle):
def set_width(self, w):
self.width = w
self.height = w # casse le contrat du parent !
def double_width(rect: Rectangle):
rect.set_width(rect.width * 2)
# avec Square, la hauteur est aussi doublee -> comportement inattendu
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
pass
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
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
I — Interface Segregation Principle¶
Ne pas forcer une classe a implémenter des méthodes dont elle n'a pas besoin.
Des interfaces trop larges créent des implémentations vides ou levant des NotImplementedError.
D — Dependency Inversion Principle¶
Dépendre des abstractions, pas des implémentations.
Les modules de haut niveau ne doivent pas dépendre des modules de bas niveau. Les deux doivent dépendre d'abstractions.
from abc import ABC, abstractmethod
class DatabasePort(ABC):
@abstractmethod
def save(self, data): pass
class MySQLDatabase(DatabasePort):
def save(self, data): ...
class InMemoryDatabase(DatabasePort): # pour les tests
def save(self, data): ...
class UserService:
def __init__(self, db: DatabasePort):
self.db = db # injection de dependance
def create_user(self, user):
self.db.save(user)
Tableau recapitulatif¶
| Principe | Règle en une ligne | Signal de violation |
|---|---|---|
| SRP | Une classe = une raison de changer | Classe avec "Et" dans sa description |
| OCP | Étendre sans modifier | if/elif qui grandit à chaque nouvelle feature |
| LSP | Sous-classe substituable sans surprise | Override qui leve une exception ou ignore |
| ISP | Interfaces minces et ciblées | Méthodes vides ou NotImplementedError |
| DIP | Dépendre des abstractions | new ConcreteClass() au milieu de la logique |
Attention à la sur-ingénierie
SOLID appliqué mecaniquement produit du code verbeux et difficile à lire. Sur un script de 50 lignes ou un prototype, ignorez-les. Sur un module partage par plusieurs équipes appelé a évoluer, ils deviennent indispensables. Appliquez-les là où la douleur est réelle, pas par principe.