Aller au contenu

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.

class ReportExporter:
    def export(self, report, format: str):
        if format == "pdf":
            # ... logique PDF
            pass
        elif format == "csv":
            # ... logique CSV
            pass
        # chaque nouveau format oblige a modifier cette classe
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.

class Worker(ABC):
    @abstractmethod
    def work(self): pass

    @abstractmethod
    def eat(self): pass  # un robot ne mange pas

class Robot(Worker):
    def work(self): print("working")
    def eat(self): raise NotImplementedError  # aberrant
class Workable(ABC):
    @abstractmethod
    def work(self): pass

class Eatable(ABC):
    @abstractmethod
    def eat(self): pass

class Human(Workable, Eatable):
    def work(self): print("working")
    def eat(self): print("eating")

class Robot(Workable):
    def work(self): print("working")

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.

class MySQLDatabase:
    def save(self, data): ...

class UserService:
    def __init__(self):
        self.db = MySQLDatabase()  # couplage dur a MySQL

    def create_user(self, user):
        self.db.save(user)
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.