Que sont les principes SOLID ? Guide pour débutants des 5 principes fondamentaux de la conception orientée objet [Avec exemples Python]

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.

💡 Astuce

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.

⚠️ Piège courant

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

mauvais_srp.py
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

bon_srp.py
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

mauvais_ocp.py
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

bon_ocp.py
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
💡 Astuce

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

mauvais_lsp.py
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

bon_lsp.py
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
⚠️ Piège courant

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

mauvais_isp.py
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

bon_isp.py
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

mauvais_dip.py
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

bon_dip.py
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.

💡 Astuce

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.

Comments

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *