POO Python Clean Architecture SOLID Course
POO Python Clean Architecture SOLID Course
Introduction
Ce cours est conçu pour vous fournir une compréhension approfondie de la Programmation
Orientée Objet (POO) en Python, en allant au-delà des concepts de base pour explorer
comment la POO s'intègre dans des architectures logicielles robustes et maintenables. Nous
mettrons un accent particulier sur la Clean Architecture et l'application des principes SOLID,
des concepts essentiels pour construire des systèmes logiciels flexibles, testables et évolutifs.
Python est un langage multi-paradigme, ce qui signifie qu'il prend en charge différents styles
de programmation, y compris la programmation procédurale, fonctionnelle et orientée objet.
La POO en Python permet de modéliser des problèmes complexes du monde réel en utilisant
des objets, qui encapsulent à la fois des données et le comportement qui leur est associé. Cela
conduit à un code plus organisé, réutilisable et facile à maintenir. [1]
La Clean Architecture, popularisée par Robert C. Martin (souvent appelé Uncle Bob), est une
approche de conception logicielle qui vise à créer des systèmes dont la logique métier est
indépendante des détails d'implémentation tels que les frameworks, les bases de données,
les interfaces utilisateur ou les services externes. Elle propose une structure en couches
concentriques, où les dépendances pointent toujours vers l'intérieur, vers le cœur de
l'application qui contient les règles métier les plus stables. [2]
"Good architectures are not about frameworks, they are about organizing your code so that
the framework is an interchangeable detail." - Robert C. Martin
Les avantages clés de la Clean Architecture incluent : * Indépendance : Le code métier est
indépendant des frameworks, des bases de données, des UI, etc. * Testabilité : La logique
métier peut être testée sans dépendre de l'infrastructure. * Maintenabilité : Les changements
dans les détails d'implémentation ont un impact minimal sur la logique métier. * Flexibilité :
Facilite le changement de technologies sous-jacentes.
Les principes SOLID sont un ensemble de cinq principes de conception orientée objet qui,
lorsqu'ils sont appliqués, aident à créer des logiciels plus compréhensibles, flexibles et
maintenables. Ils sont un pilier de la conception de systèmes robustes et sont
intrinsèquement liés à la Clean Architecture. [3]
3. Liskov Substitution Principle (LSP) - Principe de Substitution de Liskov : Les objets d'un
programme devraient pouvoir être remplacés par des instances de leurs sous-types sans
altérer la justesse de ce programme. [6]
Tout au long de ce cours, nous explorerons chacun de ces principes en détail et verrons
comment les appliquer concrètement en Python pour construire des applications bien
structurées.
Structure du Cours
Ce cours est structuré pour vous guider progressivement à travers les concepts de la POO en
Python, en intégrant les principes de la Clean Architecture et SOLID. Nous commencerons par
les bases de la POO, puis nous approfondirons les concepts avancés et les meilleures
pratiques architecturales.
Commençons notre voyage dans le monde de la POO en Python avec une architecture propre
et des principes solides.
Objectif
Contenu détaillé
En POO, une classe est un plan, un modèle ou un prototype à partir duquel des objets sont
créés. Elle définit un ensemble de propriétés (attributs) et de comportements (méthodes) que
les objets créés à partir de cette classe posséderont. Pensez à une classe comme au plan
d'une maison : le plan décrit les caractéristiques de la maison (nombre de pièces, taille, etc.)
mais n'est pas la maison elle-même. [9]
class MaPremiereClasse:
pass # 'pass' est utilisé comme un espace réservé pour une classe vide
Un objet est une instance d'une classe. C'est la réalisation concrète du plan défini par la
classe. Si la classe est le plan d'une maison, un objet est la maison réelle construite à partir de
ce plan. Vous pouvez créer plusieurs objets à partir de la même classe, et chaque objet aura
ses propres données, mais partagera les mêmes comportements définis par la classe. [10]
Pour créer un objet (ou instancier une classe) en Python, vous appelez la classe comme une
fonction :
mon_objet = MaPremiereClasse()
3. Attributs (Propriétés)
Les attributs sont les données ou les caractéristiques associées à une classe ou à un objet. Ils
représentent l'état d'un objet. Il existe deux types principaux d'attributs :
Attributs de classe : Partagés par toutes les instances de la classe. Ils sont définis
directement dans la classe, en dehors de toute méthode. [11]
Attributs d'instance : Spécifiques à chaque objet. Ils sont définis à l'intérieur des
méthodes, généralement dans la méthode __init__ (le constructeur). [12]
```python class Personne: def init(self, nom, age): [Link] = nom # Attribut d'instance
[Link] = age # Attribut d'instance
4. Méthodes (Comportements)
Les méthodes sont des fonctions définies à l'intérieur d'une classe qui décrivent les
comportements que les objets de cette classe peuvent effectuer. Elles opèrent généralement
sur les attributs de l'objet. [13]
Méthodes d'instance : Ce sont les méthodes les plus courantes. Elles prennent self
comme premier argument, qui fait référence à l'instance de l'objet sur lequel la méthode
est appelée. [14]
```python class Chien: def init(self, nom, race): [Link] = nom [Link] = race
def aboyer(self):
return f"{[Link]} le {[Link]} aboie !"
def decrire(self):
return f"Je suis {[Link]}, un {[Link]}."
méthode "dunder" ou "magic method" en raison de ses doubles underscores) qui est
automatiquement appelée lorsque vous créez une nouvelle instance d'une classe. Elle est
utilisée pour initialiser les attributs de l'objet. [15]
```python
class Livre:
def __init__(self, titre, auteur, annee_publication):
[Link] = titre
[Link] = auteur
self.annee_publication = annee_publication
def get_info(self):
return f"'{[Link]}' par {[Link]}, publié en
{self.annee_publication}."
```python class Date: def init(self, jour, mois, annee): [Link] = jour [Link] = mois
[Link] = annee
@classmethod
def from_string(cls, date_str):
jour, mois, annee = map(int, date_str.split("-"))
return cls(jour, mois, annee)
def afficher_date(self):
return f"{[Link]:02d}-{[Link]:02d}-{[Link]}"
@staticmethod
def multiplier(a, b):
return a * b
5. Résumé du Chapitre 1
Dans ce chapitre, nous avons couvert les concepts fondamentaux de la POO en Python : *
Classes comme plans pour créer des objets. * Objets comme instances concrètes des classes.
* Attributs pour stocker les données (état) des objets. * Méthodes pour définir les
comportements des objets, y compris la méthode __init__ pour l'initialisation, les
méthodes de classe et les méthodes statiques.
Ces bases sont essentielles pour comprendre les concepts plus avancés de la POO que nous
explorerons dans les chapitres suivants, notamment l'héritage, le polymorphisme,
l'encapsulation et l'abstraction.
Références du Chapitre 1
Objectif
Contenu détaillé
L'héritage est un mécanisme de la POO qui permet à une nouvelle classe (appelée classe
enfant, sous-classe ou classe dérivée) d'acquérir les attributs et les méthodes d'une classe
existante (appelée classe parent, super-classe ou classe de base). Cela favorise la
réutilisation du code et établit une relation
def faire_son(self):
raise NotImplementedError("Cette méthode doit être implémentée par les sous-
classes")
class Chien(Animal):
def __init__(self, nom, race):
super().__init__(nom) # Appelle le constructeur de la classe parente
[Link] = race
def faire_son(self):
return f"{[Link]} aboie."
class Chat(Animal):
def __init__(self, nom):
super().__init__(nom)
def faire_son(self):
return f"{[Link]} miaule."
Dans cet exemple, Chien et Chat héritent de la classe Animal . Ils réutilisent l'attribut nom et
implémentent leur propre version de la méthode faire_son() . [19]
Le polymorphisme (du grec "poly" signifiant plusieurs et "morph" signifiant formes) est la
capacité d'un objet à prendre plusieurs formes. En POO, cela se manifeste par la possibilité
d'utiliser une interface commune pour différentes implémentations. Cela signifie que des
objets de classes différentes peuvent être traités de manière uniforme s'ils partagent une
interface commune (par exemple, s'ils implémentent la même méthode). [20]
Exemple de polymorphisme :
def interagir_avec_animal(animal):
print(animal.faire_son())
# Un autre exemple avec une classe non liée par héritage mais partageant la même
méthode
class Canard:
def __init__(self, nom):
[Link] = nom
def faire_son(self):
return f"{[Link]} cancane."
mon_canard = Canard("Daffy")
interagir_avec_animal(mon_canard) # Output: Daffy cancane.
Le polymorphisme permet d'écrire du code plus générique et flexible, car vous pouvez
travailler avec des objets sans connaître leur type exact, tant qu'ils fournissent le
comportement attendu. [21]
Le Principe de Substitution de Liskov (LSP), formulé par Barbara Liskov, est un principe clé
de SOLID qui est étroitement lié à l'héritage et au polymorphisme. Il stipule que les objets
d'un programme devraient pouvoir être remplacés par des instances de leurs sous-types sans
altérer la justesse de ce programme. En d'autres termes, si S est un sous-type de T , alors les
objets de type T peuvent être remplacés par des objets de type S sans casser le programme.
[22]
@property
def largeur(self):
return self._largeur
@[Link]
def largeur(self, value):
self._largeur = value
@property
def hauteur(self):
return self._hauteur
@[Link]
def hauteur(self, value):
self._hauteur = value
def aire(self):
return self._largeur * self._hauteur
class Carre(Rectangle):
def __init__(self, cote):
super().__init__(cote, cote)
@[Link]
def largeur(self, value):
self._largeur = value
self._hauteur = value # Violation: changer la largeur change aussi la hauteur
@[Link]
def hauteur(self, value):
self._hauteur = value
self._largeur = value # Violation: changer la hauteur change aussi la largeur
def augmenter_largeur(rectangle):
[Link] = 10
# On s'attend à ce que la hauteur reste inchangée pour un Rectangle
# Mais pour un Carre, la hauteur changera aussi, violant le LSP
print(f"Largeur: {[Link]}, Hauteur: {[Link]}, Aire:
{[Link]()}")
rect = Rectangle(5, 4)
carre = Carre(5)
Dans cet exemple, la classe Carre viole le LSP car elle modifie le comportement attendu des
setters de Rectangle . Un Carre n'est pas un Rectangle dans le sens où il ne peut pas être
substitué à un Rectangle sans altérer le comportement du programme. Pour respecter le
LSP, il est souvent préférable de ne pas faire hériter Carre de Rectangle , ou de repenser la
hiérarchie pour que les deux héritent d'une abstraction plus générale comme Forme . [23]
Respect du LSP : Pour respecter le LSP, les sous-classes ne doivent pas modifier les
préconditions, les postconditions ou les invariants de la super-classe. Elles doivent maintenir
le contrat défini par la super-classe. [24]
Le Principe Ouvert/Fermé (OCP), un autre principe fondamental de SOLID, stipule que les
entités logicielles (classes, modules, fonctions, etc.) devraient être ouvertes à l'extension,
mais fermées à la modification. Cela signifie que le comportement d'un module peut être
étendu sans modifier son code source existant. [25]
class Employe:
def __init__(self, nom, salaire_base):
[Link] = nom
self.salaire_base = salaire_base
class CalculateurSalaire:
def calculer_salaire(self, employe):
if isinstance(employe, Employe):
return employe.salaire_base
elif isinstance(employe, Manager):
return employe.salaire_base + [Link]
# ... et si on ajoute un nouveau type d'employé, il faut modifier cette classe
class Manager(Employe):
def __init__(self, nom, salaire_base, bonus):
super().__init__(nom, salaire_base)
[Link] = bonus
Cette approche viole l'OCP car chaque fois qu'un nouveau type d'employé est introduit, la
classe CalculateurSalaire doit être modifiée. [26]
Respect de l'OCP avec le Polymorphisme : Pour respecter l'OCP, nous pouvons utiliser le
polymorphisme. Chaque type d'employé est responsable de son propre calcul de salaire.
class Employe:
def __init__(self, nom, salaire_base):
[Link] = nom
self.salaire_base = salaire_base
def calculer_salaire(self):
return self.salaire_base
class Manager(Employe):
def __init__(self, nom, salaire_base, bonus):
super().__init__(nom, salaire_base)
[Link] = bonus
def calculer_salaire(self):
return self.salaire_base + [Link]
class Stagiaire(Employe):
def __init__(self, nom, salaire_base, indemnites):
super().__init__(nom, salaire_base)
[Link] = indemnites
def calculer_salaire(self):
return self.salaire_base + [Link]
def afficher_salaires(employes):
for emp in employes:
print(f"Salaire de {[Link]}: {emp.calculer_salaire()}€")
employes = [
Employe("Alice", 2000),
Manager("Bob", 3000, 500),
Stagiaire("Charlie", 800, 200)
]
afficher_salaires(employes)
# Si un nouveau type d'employé est ajouté, seule sa classe doit être modifiée/ajoutée,
pas la fonction afficher_salaires.
Ici, la fonction afficher_salaires est fermée à la modification (elle n'a pas besoin d'être
changée pour gérer de nouveaux types d'employés) mais le système est ouvert à l'extension
(de nouveaux types d'employés peuvent être ajoutés en créant de nouvelles classes qui
implémentent la méthode calculer_salaire ). C'est l'essence de l'OCP. [27]
5. Résumé du Chapitre 2
Références du Chapitre 2
Objectif
Contenu détaillé
L'encapsulation est l'un des piliers fondamentaux de la Programmation Orientée Objet. Elle
consiste à regrouper les données (attributs) et les méthodes (fonctions) qui opèrent sur ces
données en une seule unité, l'objet. L'objectif principal de l'encapsulation est de cacher les
détails d'implémentation internes d'un objet et de n'exposer qu'une interface publique bien
définie pour interagir avec lui. Cela permet de protéger l'intégrité des données et de réduire la
complexité du système. [28]
class CompteBancaire:
def __init__(self, solde_initial):
self._solde = solde_initial # Attribut "protégé" par convention
self.__numero_compte = "123456789" # Attribut "privé" par convention (name
mangling)
def get_solde(self):
return self._solde
# Utilisation
mon_compte = CompteBancaire(1000)
mon_compte.deposer(200)
mon_compte.retirer(500)
print(f"Solde actuel : {mon_compte.get_solde()}€.")
Les propriétés en Python offrent un moyen élégant de gérer l'accès aux attributs d'une classe,
permettant d'ajouter une logique (validation, calcul, etc.) lors de la lecture ou de la
modification d'un attribut, sans changer la manière dont l'attribut est accédé de l'extérieur.
Elles transforment des méthodes en attributs, respectant ainsi le Principe de Responsabilité
Unique (SRP) en séparant la logique de stockage de la logique d'accès. [32]
class Cercle:
def __init__(self, rayon):
self._rayon = rayon # Attribut interne
@property
def rayon(self):
"""Le getter pour l'attribut rayon."""
return self._rayon
@[Link]
def rayon(self, value):
"""Le setter pour l'attribut rayon avec validation."""
if value < 0:
raise ValueError("Le rayon ne peut pas être négatif.")
self._rayon = value
@property
def aire(self):
"""Calcule l'aire du cercle (lecture seule)."""
import math
return [Link] * (self._rayon ** 2)
# Utilisation
c = Cercle(5)
print(f"Rayon initial : {[Link]}") # Accès via le getter
print(f"Aire initiale : {[Link]}") # Accès via la propriété calculée
try:
[Link] = -2 # Déclenche une ValueError
except ValueError as e:
print(e)
# try:
# [Link] = 10 # Erreur: AttributeError, car 'aire' est une propriété en lecture
seule
# except AttributeError as e:
# print(e)
@abstractmethod
def perimetre(self):
pass # Méthode abstraite
def decrire(self):
return "Ceci est une forme géométrique."
class Rectangle(Forme):
def __init__(self, largeur, hauteur):
[Link] = largeur
[Link] = hauteur
def aire(self):
return [Link] * [Link]
def perimetre(self):
return 2 * ([Link] + [Link])
class Cercle(Forme):
def __init__(self, rayon):
[Link] = rayon
def aire(self):
import math
return [Link] * ([Link] ** 2)
def perimetre(self):
import math
return 2 * [Link] * [Link]
# Utilisation
rect = Rectangle(10, 5)
print(f"Rectangle - Aire: {[Link]()}, Périmètre: {[Link]()}")
print([Link]())
cercle = Cercle(7)
print(f"Cercle - Aire: {[Link]():.2f}, Périmètre: {[Link]():.2f}")
print([Link]())
# try:
# forme = Forme() # Erreur: TypeError, ne peut pas instancier une classe abstraite
# except TypeError as e:
# print(e)
L'abstraction permet de définir des contrats (interfaces) que les classes concrètes doivent
respecter, ce qui est crucial pour le Principe de Ségrégation des Interfaces (ISP). [36]
4. Principe de Ségrégation des Interfaces (ISP)
Le Principe de Ségrégation des Interfaces (ISP), un autre principe de SOLID, stipule qu'un
client ne devrait pas être forcé de dépendre d'interfaces qu'il n'utilise pas. En d'autres termes,
il est préférable d'avoir de nombreuses interfaces spécifiques au client plutôt qu'une seule
interface générale. [37]
Violation de l'ISP : Imaginez une interface Travailleur avec toutes les méthodes possibles
pour différents types de travailleurs.
class Travailleur(ABC):
@abstractmethod
def travailler(self):
pass
@abstractmethod
def manger(self):
pass
@abstractmethod
def dormir(self):
pass
@abstractmethod
def gerer_equipe(self):
pass
@abstractmethod
def coder(self):
pass
class Developpeur(Travailleur):
def travailler(self):
print("Le développeur travaille.")
def manger(self):
print("Le développeur mange.")
def dormir(self):
print("Le développeur dort.")
def gerer_equipe(self):
# Le développeur n'a pas cette responsabilité, mais doit l'implémenter
raise NotImplementedError("Le développeur ne gère pas d'équipe.")
def coder(self):
print("Le développeur code.")
Ici, la classe Developpeur est forcée d'implémenter la méthode gerer_equipe même si elle
n'est pas pertinente pour un développeur. Cela rend le code moins maintenable et plus sujet
aux erreurs. [38]
Respect de l'ISP : Pour respecter l'ISP, nous devrions diviser l'interface Travailleur en
interfaces plus petites et plus spécifiques.
class TravailleurGeneral(ABC):
@abstractmethod
def travailler(self):
pass
@abstractmethod
def manger(self):
pass
@abstractmethod
def dormir(self):
pass
class Gerant(ABC):
@abstractmethod
def gerer_equipe(self):
pass
class Codeur(ABC):
@abstractmethod
def coder(self):
pass
def manger(self):
print("Le développeur mange.")
def dormir(self):
print("Le développeur dort.")
def coder(self):
print("Le développeur code.")
def manger(self):
print("Le chef de projet mange.")
def dormir(self):
print("Le chef de projet dort.")
def gerer_equipe(self):
print("Le chef de projet gère l'équipe.")
# Utilisation
dev = Developpeur()
[Link]()
[Link]()
chef = ChefDeProjet()
[Link]()
chef.gerer_equipe()
En divisant l'interface, chaque classe n'implémente que les méthodes qui lui sont réellement
nécessaires, ce qui rend le système plus robuste et plus facile à maintenir. [39]
5. Résumé du Chapitre 3
Références du Chapitre 3
Objectif
Explorer les méthodes spéciales (ou "dunder methods") en Python, qui permettent de définir
le comportement des objets pour les opérations intégrées du langage. Nous aborderons
également d'autres concepts avancés de la POO en Python.
Contenu détaillé
Voici quelques-unes des dunder methods les plus courantes et leur utilité :
def __repr__(self):
return f"Point(x={self.x}, y={self.y})"
def __len__(self):
return len([Link])
def __str__(self):
return f"Vecteur({self.x}, {self.y})"
```python class Personne: def init(self, nom, age): [Link] = nom [Link] = age
L'utilisation judicieuse des dunder methods peut rendre vos classes plus intuitives et plus
"pythoniques".
Bien que l'héritage soit un outil puissant pour la réutilisation du code, il peut parfois conduire
à des hiérarchies de classes rigides et à des problèmes comme le "problème du diamant" ou
des violations du LSP. La composition est une alternative à l'héritage qui consiste à construire
des objets complexes en combinant des objets plus simples. Au lieu d'une relation "est un
type de" (héritage), la composition représente une relation "a un" (has-a). [49]
Exemple :
# Héritage
class Moteur:
def demarrer(self):
print("Moteur démarré.")
class Voiture(Moteur):
def rouler(self):
[Link]()
print("Voiture roule.")
# Composition
class Moteur:
def demarrer(self):
print("Moteur démarré.")
class Voiture:
def __init__(self):
[Link] = Moteur() # Voiture a un Moteur
def rouler(self):
[Link]()
print("Voiture roule.")
ma_voiture = Voiture()
ma_voiture.rouler()
La composition est souvent préférée à l'héritage pour les raisons suivantes : * Flexibilité : Il est
plus facile de changer le comportement d'un objet en remplaçant l'objet composé plutôt
qu'en modifiant la hiérarchie d'héritage. * Moins de couplage : Les classes sont moins
couplées, ce qui réduit les dépendances et facilite la maintenance. * Respect du SRP : Chaque
composant a une responsabilité unique.
Il est souvent dit : "Préférer la composition à l'héritage" (Prefer composition over inheritance).
[50]
Les design patterns sont des solutions générales et réutilisables à des problèmes courants de
conception logicielle. Ils ne sont pas des solutions prêtes à l'emploi, mais des modèles qui
peuvent être adaptés à des situations spécifiques. Comprendre les design patterns peut vous
aider à écrire du code plus propre, plus maintenable et plus évolutif. [51]
Singleton : Garantit qu'une classe n'a qu'une seule instance et fournit un point d'accès
global à cette instance. Utile pour les gestionnaires de configuration, les pools de
connexions, etc. [52]
Factory Method : Définit une interface pour créer un objet, mais laisse les sous-classes
décider quelle classe instancier. Permet de déléguer la logique de création d'objets aux
sous-classes. [53]
def une_operation(self):
produit = self.factory_method()
result = f"Createur: {[Link]()}"
return result
Observer : Définit une dépendance un-à-plusieurs entre des objets de telle sorte que
lorsqu'un objet change d'état, tous ses dépendants sont avertis et mis à jour
automatiquement. Utile pour les systèmes d'événements. [54]
[Link](obs1) [Link](obs2)
Output:
Ces patterns ne sont qu'un aperçu. Il existe de nombreux autres patterns qui peuvent vous
aider à résoudre des problèmes de conception complexes de manière élégante et efficace.
4. Résumé du Chapitre 4
Références du Chapitre 4
Objectif
Contenu détaillé
Difficulté de Test : Tester la logique métier devient complexe car elle est inséparable de
ses dépendances externes.
Manque de Portabilité : Le code métier est difficile à réutiliser dans d'autres contextes
ou avec d'autres technologies.
La Clean Architecture propose une solution à ces problèmes en organisant le code en couches
concentriques, avec des règles strictes sur les dépendances. L'idée centrale est que les
dépendances doivent toujours pointer vers l'intérieur, vers les couches les plus stables et les
plus abstraites. [57]
Les quatre cercles principaux de la Clean Architecture, de l'intérieur vers l'extérieur, sont :
Entités (Entities) : Le cœur de l'application. Elles contiennent les règles métier les plus
générales et de haut niveau. Elles sont indépendantes de tout changement externe. Ce
sont les objets métier purs, comme Utilisateur , Produit , Commande . [58]
En Python, nous pouvons structurer notre projet en répertoires qui représentent ces couches.
L'utilisation de l'injection de dépendances et des interfaces (classes abstraites) est cruciale
pour maintenir la règle de dépendance. [63]
Structure de Répertoire Typique :
mon_application/
├── core/ # Couche Entités (Domaine)
│ ├── [Link] # Entités métier pures
│ └── [Link] # Interfaces (abstractions) pour les repositories, services
externes
├── application/ # Couche Cas d'Utilisation
│ ├── use_cases.py # Logique métier spécifique à l'application
│ └── [Link] # Data Transfer Objects
├── infrastructure/ # Couche Adaptateurs d'Interface (Implémentations
concrètes)
│ ├── repositories/ # Implémentations des interfaces de repository (ex:
SQLAlchemy, Django ORM)
│ ├── services/ # Implémentations des services externes
│ └── __init__.py
├── presentation/ # Couche Frameworks et Pilotes (Interface Utilisateur/API)
│ ├── api/ # API REST (ex: FastAPI, Flask, Django REST Framework)
│ ├── web/ # Interface utilisateur web (ex: templates Jinja, React)
│ └── __init__.py
├── tests/
└── [Link] # Point d'entrée de l'application (composition root)
core/ (Entités) : Contient les classes Python pures qui représentent les concepts métier
fondamentaux. Elles ne doivent avoir aucune dépendance envers d'autres couches. Elles
peuvent définir des interfaces abstraites (utilisant [Link] ) pour les services externes
ou les repositories dont elles ont besoin, mais elles ne connaissent pas leurs
implémentations concrètes. [64]
Exemple :
# core/[Link]
from abc import ABC, abstractmethod
class IUserRepository(ABC):
@abstractmethod
def get_user_by_id(self, user_id: str):
pass
@abstractmethod
def save_user(self, user):
pass
# application/use_cases.py
from [Link] import IUserRepository
class GetUserUseCase:
def __init__(self, user_repository: IUserRepository): # Injection de dépendance
self.user_repository = user_repository
# infrastructure/repositories/in_memory_user_repository.py
from [Link] import IUserRepository
class InMemoryUserRepository(IUserRepository):
def __init__(self):
self._users = {}
# Utilisation
# user = get_user_use_case.execute("some_id")
Dans cet exemple, GetUserUseCase ne connaît que l'interface IUserRepository , pas son
implémentation concrète ( InMemoryUserRepository ). L'implémentation est "injectée" au
moment de la création de GetUserUseCase . Cela permet de changer facilement
l'implémentation du repository (par exemple, passer à une base de données SQL) sans
modifier le code du cas d'utilisation. [69]
5. Résumé du Chapitre 5
Références du Chapitre 5
[55] Martin, R. C. (2017). Clean Architecture: A Craftsman's Guide to Software Structure and
Design. Pearson Education. (Chapter 1, What is Architecture?) [56] Martin, R. C. (2017). Clean
Architecture: A Craftsman's Guide to Software Structure and Design. Pearson Education.
(Chapter 2, The Goal of Architecture) [57] Martin, R. C. (2017). Clean Architecture: A
Craftsman's Guide to Software Structure and Design. Pearson Education. (Chapter 22, The
Clean Architecture) [58] Martin, R. C. (2017). Clean Architecture: A Craftsman's Guide to
Software Structure and Design. Pearson Education. (Chapter 22, The Clean Architecture -
Entities) [59] Martin, R. C. (2017). Clean Architecture: A Craftsman's Guide to Software
Structure and Design. Pearson Education. (Chapter 22, The Clean Architecture - Use Cases)
[60] Martin, R. C. (2017). Clean Architecture: A Craftsman's Guide to Software Structure and
Design. Pearson Education. (Chapter 22, The Clean Architecture - Interface Adapters) [61]
Martin, R. C. (2017). Clean Architecture: A Craftsman's Guide to Software Structure and Design.
Pearson Education. (Chapter 22, The Clean Architecture - Frameworks and Drivers) [62] Martin,
R. C. (2017). Clean Architecture: A Craftsman's Guide to Software Structure and Design.
Pearson Education. (Chapter 22, The Clean Architecture - The Dependency Rule) [63] Real
Python. (n.d.). Dependency Injection in Python. Consulté le 28 juin 2025, de
[Link] [64] Martin, R. C. (2017). Clean
Architecture: A Craftsman's Guide to Software Structure and Design. Pearson Education.
(Chapter 22, The Clean Architecture - Entities) [65] Martin, R. C. (2017). Clean Architecture: A
Craftsman's Guide to Software Structure and Design. Pearson Education. (Chapter 22, The
Clean Architecture - Use Cases) [66] Martin, R. C. (2017). Clean Architecture: A Craftsman's
Guide to Software Structure and Design. Pearson Education. (Chapter 22, The Clean
Architecture - Interface Adapters) [67] Martin, R. C. (2017). Clean Architecture: A Craftsman's
Guide to Software Structure and Design. Pearson Education. (Chapter 22, The Clean
Architecture - Frameworks and Drivers) [68] Real Python. (n.d.). Dependency Injection in
Python. Consulté le 28 juin 2025, de [Link]
[69] Real Python. (n.d.). Dependency Injection in Python. Consulté le 28 juin 2025, de
[Link]
Objectif
Contenu détaillé
La Clean Architecture et les principes SOLID sont des concepts complémentaires qui
travaillent en synergie pour créer des systèmes logiciels robustes, flexibles et maintenables.
La Clean Architecture fournit la structure globale, tandis que SOLID guide la conception des
composants individuels au sein de cette structure. [70]
Le Principe de Responsabilité Unique (SRP) stipule qu'une classe ne devrait avoir qu'une
seule raison de changer. Dans le contexte de la Clean Architecture, cela se traduit par une
séparation claire des préoccupations entre les couches et au sein des composants de chaque
couche. [71]
Les entités ont pour seule responsabilité de contenir les règles métier de haut
niveau et l'état. Elles ne devraient pas se soucier de la persistance, de la
présentation ou de la logique d'application spécifique. Par exemple, une classe
Produit ne devrait pas contenir de logique pour sauvegarder un produit dans une
base de données ou l'afficher sur une interface utilisateur. [72]
Chaque cas d'utilisation (use case) a une seule responsabilité : orchestrer une
fonctionnalité spécifique de l'application. Par exemple, un
CreerUtilisateurUseCase est responsable de la création d'un utilisateur, et non
de la validation des données d'entrée (qui est la responsabilité d'un DTO ou d'un
validateur) ou de la persistance (responsabilité du repository). [73]
Les adaptateurs de repository ont pour seule responsabilité de convertir les objets
du domaine vers et depuis le format de la base de données, et d'interagir avec le
système de persistance. Ils ne contiennent pas de logique métier. [74]
Exemple (SRP) :
# core/[Link]
class Commande:
def __init__(self, id, articles, statut="en_attente"):
[Link] = id
[Link] = articles
[Link] = statut
def calculer_total(self):
return sum([Link] * [Link] for article in [Link])
def valider_commande(self):
if not [Link]:
raise ValueError("La commande doit contenir des articles.")
# Autres règles métier de validation de commande
# application/use_cases.py
from [Link] import Commande
from [Link] import ICommandeRepository # Supposons cette interface
class PlacerCommandeUseCase:
def __init__(self, commande_repo: ICommandeRepository):
self.commande_repo = commande_repo
# infrastructure/repositories/sql_commande_repository.py
from [Link] import ICommandeRepository
# from database_orm import CommandeModel # Modèle ORM
class SQLCommandeRepository(ICommandeRepository):
def save(self, commande: Commande):
# Logique pour sauvegarder la commande dans la base de données via ORM
# CommandeModel.create_from_entity(commande).save()
pass
Chaque composant a une responsabilité claire, ce qui rend le système plus facile à
comprendre, à tester et à modifier. [76]
Le Principe Ouvert/Fermé (OCP) stipule que les entités logicielles devraient être ouvertes à
l'extension, mais fermées à la modification. La Clean Architecture favorise naturellement
l'OCP grâce à sa structure en couches et à l'utilisation d'interfaces. [77]
Par exemple, si vous ajoutez un nouveau type de notification (e-mail, SMS, push),
vous créez une nouvelle implémentation de l'interface INotificationService
(définie dans core/[Link] ) dans la couche infrastructure , sans
modifier les cas d'utilisation qui dépendent de cette interface. [79]
Exemple (OCP) :
# core/[Link]
from abc import ABC, abstractmethod
class ILogger(ABC):
@abstractmethod
def log_info(self, message: str):
pass
@abstractmethod
def log_error(self, message: str):
pass
# application/use_cases.py
from [Link] import ILogger
class TraiterDonneesUseCase:
def __init__(self, logger: ILogger):
[Link] = logger
# infrastructure/console_logger.py
from [Link] import ILogger
class ConsoleLogger(ILogger):
def log_info(self, message: str):
print(f"[INFO] {message}")
class FileLogger(ILogger):
def __init__(self, filename="[Link]"):
[Link] = filename
Le Principe de Substitution de Liskov (LSP) garantit que les sous-types peuvent être
substitués à leurs types de base sans altérer la justesse du programme. Dans la Clean
Architecture, le LSP est crucial pour la flexibilité des implémentations. [81]
Implémentations de Repository :
Toutes les implémentations concrètes d'un repository (par exemple,
SQLUserRepository , InMemoryUserRepository ) doivent respecter le contrat
défini par leur interface ( IUserRepository ). Cela signifie qu'elles doivent
implémenter toutes les méthodes de l'interface et se comporter de manière
cohérente avec les attentes de cette interface. [82]
Exemple (LSP) :
# core/[Link] (déjà défini)
# class IUserRepository(ABC):
# @abstractmethod
# def get_user_by_id(self, user_id: str):
# pass
# @abstractmethod
# def save_user(self, user):
# pass
class DatabaseUserRepository(IUserRepository):
def get_user_by_id(self, user_id: str):
# Logique pour récupérer l'utilisateur de la base de données
# user_model = [Link](id=user_id)
# return User.from_model(user_model) # Convertir en entité du domaine
pass
# repo_database = DatabaseUserRepository()
# get_user_uc_db = GetUserUseCase(user_repository=repo_database)
Le LSP assure que le code des couches internes (comme les cas d'utilisation) n'a pas besoin de
savoir quelle implémentation concrète est utilisée, car toutes les implémentations respectent
le même contrat. [84]
De même, pour les services externes, si un cas d'utilisation n'a besoin que
d'envoyer des e-mails, il dépendra d'une interface IEmailSender et non d'une
interface INotificationService qui inclut aussi des méthodes pour les SMS et les
notifications push. [87]
Exemple (ISP) :
# core/[Link]
from abc import ABC, abstractmethod
class IEmailSender(ABC):
@abstractmethod
def send_email(self, recipient: str, subject: str, body: str):
pass
class ISMSSender(ABC):
@abstractmethod
def send_sms(self, recipient_phone: str, message: str):
pass
# application/use_cases.py
from [Link] import IEmailSender
class EnregistrerUtilisateurUseCase:
def __init__(self, user_repo, email_sender: IEmailSender):
self.user_repo = user_repo
self.email_sender = email_sender
# infrastructure/email_service.py
from [Link] import IEmailSender
class SmtpEmailSender(IEmailSender):
def send_email(self, recipient: str, subject: str, body: str):
print(f"Envoi d'email à {recipient}: {subject} - {body}")
# Logique réelle d'envoi d'email via SMTP
# infrastructure/sms_service.py
from [Link] import ISMSSender
class TwilioSMSSender(ISMSSender):
def send_sms(self, recipient_phone: str, message: str):
print(f"Envoi de SMS à {recipient_phone}: {message}")
# Logique réelle d'envoi de SMS via Twilio API
L'ISP, en conjonction avec la Clean Architecture, réduit le couplage en s'assurant que les
composants ne dépendent que des interfaces minimales dont ils ont réellement besoin. [88]
Exemple (DIP) :
# core/[Link] (Abstractions)
# class IUserRepository(ABC):
# @abstractmethod
# def get_user_by_id(self, user_id: str):
# pass
# @abstractmethod
# def save_user(self, user):
# pass
Le DIP est le moteur de l'indépendance des couches dans la Clean Architecture. Il permet de
changer les technologies sous-jacentes (base de données, framework web) sans affecter la
logique métier centrale. [93]
6. Résumé du Chapitre 6
Ce chapitre a démontré comment les cinq principes SOLID sont intrinsèquement liés et
appliqués au sein de la Clean Architecture. En respectant le SRP, l'OCP, le LSP, l'ISP et le DIP,
nous construisons des applications Python qui sont non seulement bien structurées en
couches, mais aussi hautement modulaires, testables, maintenables et extensibles. Ces
principes guident la conception de chaque composant, assurant que l'ensemble du système
reste flexible face aux changements et aux évolutions futures. Le prochain chapitre présentera
un exemple concret de microservice Python intégrant tous ces concepts.
Références du Chapitre 6
[70] Martin, R. C. (2017). Clean Architecture: A Craftsman's Guide to Software Structure and
Design. Pearson Education. (Chapter 22, The Clean Architecture) [71] Martin, R. C. (n.d.). Single
Responsibility Principle. Consulté le 28 juin 2025, de [Link]
responsibility_principle [72] Martin, R. C. (2017). Clean Architecture: A Craftsman's Guide to
Software Structure and Design. Pearson Education. (Chapter 22, The Clean Architecture -
Entities) [73] Martin, R. C. (2017). Clean Architecture: A Craftsman's Guide to Software
Structure and Design. Pearson Education. (Chapter 10, Use Cases) [74] Martin, R. C. (2017).
Clean Architecture: A Craftsman's Guide to Software Structure and Design. Pearson Education.
(Chapter 11, Presenters and Gateways) [75] Martin, R. C. (2017). Clean Architecture: A
Craftsman's Guide to Software Structure and Design. Pearson Education. (Chapter 11,
Presenters and Gateways) [76] Martin, R. C. (2017). Clean Architecture: A Craftsman's Guide to
Software Structure and Design. Pearson Education. (Chapter 22, The Clean Architecture) [77]
Martin, R. C. (n.d.). Open/closed principle. Consulté le 28 juin 2025, de
[Link] [78] Martin, R. C. (2017). Clean
Architecture: A Craftsman's Guide to Software Structure and Design. Pearson Education.
(Chapter 22, The Clean Architecture) [79] Martin, R. C. (2017). Clean Architecture: A Craftsman's
Guide to Software Structure and Design. Pearson Education. (Chapter 22, The Clean
Architecture) [80] Real Python. (n.d.). The Open/Closed Principle in Python. Consulté le 28 juin
2025, de [Link] [81] Martin, R. C. (n.d.). Liskov
substitution principle. Consulté le 28 juin 2025, de
[Link] [82] Martin, R. C. (2017). Clean
Architecture: A Craftsman's Guide to Software Structure and Design. Pearson Education.
(Chapter 12, Test Boundaries) [83] Martin, R. C. (2017). Clean Architecture: A Craftsman's Guide
to Software Structure and Design. Pearson Education. (Chapter 12, Test Boundaries) [84]
GeeksforGeeks. (n.d.). Liskov Substitution Principle in Python. Consulté le 28 juin 2025, de
[Link] [85] Martin, R. C.
(n.d.). Interface segregation principle. Consulté le 28 juin 2025, de
[Link] [86] Martin, R. C. (2017). Clean
Architecture: A Craftsman's Guide to Software Structure and Design. Pearson Education.
(Chapter 11, Presenters and Gateways) [87] Martin, R. C. (2017). Clean Architecture: A
Craftsman's Guide to Software Structure and Design. Pearson Education. (Chapter 11,
Presenters and Gateways) [88] Real Python. (n.d.). The Interface Segregation Principle in
Python. Consulté le 28 juin 2025, de [Link]
[89] Martin, R. C. (n.d.). Dependency inversion principle. Consulté le 28 juin 2025, de
[Link] [90] Martin, R. C. (2017). Clean
Architecture: A Craftsman's Guide to Software Structure and Design. Pearson Education.
(Chapter 22, The Clean Architecture) [91] Martin, R. C. (2017). Clean Architecture: A Craftsman's
Guide to Software Structure and Design. Pearson Education. (Chapter 22, The Clean
Architecture) [92] Martin, R. C. (2017). Clean Architecture: A Craftsman's Guide to Software
Structure and Design. Pearson Education. (Chapter 22, The Clean Architecture) [93] Real
Python. (n.d.). Dependency Injection in Python. Consulté le 28 juin 2025, de
[Link]
Objectif
Appliquer tous les concepts appris (POO, Clean Architecture, principes SOLID) à la
construction d'un microservice Python concret. Nous allons créer un microservice simple de
gestion de produits, en séparant clairement les couches et en respectant les principes de
conception.
Nous allons développer un microservice pour gérer des produits. Ce microservice permettra
de : * Créer un nouveau produit. * Récupérer un produit par son ID. * Lister tous les produits. *
Mettre à jour un produit existant. * Supprimer un produit.
Nous utiliserons une base de données en mémoire pour simplifier l'exemple, mais la structure
permettra de basculer facilement vers une base de données persistante (par exemple,
PostgreSQL avec SQLAlchemy) grâce à la Clean Architecture et au DIP.
Structure du Projet
Cette couche contient les règles métier les plus fondamentales et les abstractions nécessaires
pour les interactions avec les couches externes. Elle est indépendante de toute technologie
spécifique.
product_microservice/core/[Link]
import uuid
from dataclasses import dataclass
@dataclass
class Product:
id: str
name: str
description: str
price: float
stock: int
@classmethod
def create(cls, name: str, description: str, price: float, stock: int):
if not name or not description or price <= 0 or stock < 0:
raise ValueError("Invalid product data")
return cls(id=str(uuid.uuid4()), name=name, description=description,
price=price, stock=stock)
def update_info(self, name: str = None, description: str = None, price: float =
None, stock: int = None):
if name is not None: [Link] = name
if description is not None: [Link] = description
if price is not None:
if price <= 0: raise ValueError("Price must be positive")
[Link] = price
if stock is not None:
if stock < 0: raise ValueError("Stock cannot be negative")
[Link] = stock
Explication :
Product est une entité métier pure. Elle contient les données et les règles métier
(par exemple, validation des données à la création, règles pour la mise à jour du
stock). [94]
product_microservice/core/[Link]
from abc import ABC, abstractmethod
from typing import List, Optional
from [Link] import Product
class IProductRepository(ABC):
@abstractmethod
def add(self, product: Product) -> None:
pass
@abstractmethod
def get_by_id(self, product_id: str) -> Optional[Product]:
pass
@abstractmethod
def get_all(self) -> List[Product]:
pass
@abstractmethod
def update(self, product: Product) -> None:
pass
@abstractmethod
def delete(self, product_id: str) -> None:
pass
Explication :
IProductRepository est une interface abstraite qui définit le contrat pour tout
repository de produits. Elle respecte l'ISP en étant spécifique aux opérations sur les
produits. [95]
Cette couche contient la logique métier spécifique à l'application. Elle orchestre les entités et
utilise les interfaces pour interagir avec les couches externes.
product_microservice/application/[Link]
from dataclasses import dataclass
from typing import Optional
@dataclass
class ProductCreateDTO:
name: str
description: str
price: float
stock: int
@dataclass
class ProductUpdateDTO:
id: str
name: Optional[str] = None
description: Optional[str] = None
price: Optional[float] = None
stock: Optional[int] = None
@dataclass
class ProductResponseDTO:
id: str
name: str
description: str
price: float
stock: int
Explication :
Les DTOs (Data Transfer Objects) sont des objets simples utilisés pour transférer
des données entre les couches. Ils ne contiennent aucune logique métier. [96]
product_microservice/application/use_cases.py
from typing import List, Optional
from [Link] import Product
from [Link] import IProductRepository
from [Link] import ProductCreateDTO, ProductUpdateDTO, ProductResponseDTO
class ProductUseCases:
def __init__(self, product_repo: IProductRepository):
self.product_repo = product_repo
product.update_info(
name=[Link],
description=[Link],
price=[Link],
stock=[Link]
)
self.product_repo.update(product)
return ProductResponseDTO(
id=[Link],
name=[Link],
description=[Link],
price=[Link],
stock=[Link]
)
Explication :
ProductUseCases contient la logique métier de l'application. Il dépend de
IProductRepository (abstraction), respectant le DIP. [97]
Les méthodes acceptent des DTOs en entrée et retournent des DTOs en sortie,
assurant une séparation claire avec la couche de présentation.
Cette couche contient les implémentations concrètes des interfaces définies dans la couche
core . C'est ici que les détails techniques (comme l'accès à la base de données) sont gérés.
product_microservice/infrastructure/repositories/in_memory_product_repository.py
class InMemoryProductRepository(IProductRepository):
def __init__(self):
self._products = {}
Explication :
InMemoryProductRepository implémente l'interface IProductRepository . Il gère
la persistance des produits en mémoire. [99]
Cette classe est un détail d'implémentation et ne doit pas être connue des couches
core ou application , respectant le DIP.
Si nous voulions utiliser une base de données SQL, nous créerions une classe
SQLProductRepository qui implémenterait la même interface
IProductRepository .
Cette couche est la plus externe et gère l'interaction avec le monde extérieur. Elle utilise les
cas d'utilisation de la couche application .
product_microservice/presentation/[Link]
from flask import Flask, request, jsonify
from application.use_cases import ProductUseCases
from [Link] import ProductCreateDTO, ProductUpdateDTO
from [Link].in_memory_product_repository import
InMemoryProductRepository
app = Flask(__name__)
@[Link]('/products', methods=['POST'])
def create_product():
data = request.get_json()
try:
dto = ProductCreateDTO(
name=data['name'],
description=data['description'],
price=data['price'],
stock=data['stock']
)
response_dto = product_use_cases.create_product(dto)
return jsonify(response_dto.__dict__), 201
except ValueError as e:
return jsonify({'error': str(e)}), 400
except KeyError as e:
return jsonify({'error': f'Missing field: {e}'}), 400
@[Link]('/products/<string:product_id>', methods=['GET'])
def get_product(product_id):
response_dto = product_use_cases.get_product_by_id(product_id)
if response_dto:
return jsonify(response_dto.__dict__), 200
return jsonify({'message': 'Product not found'}), 404
@[Link]('/products', methods=['GET'])
def get_all_products():
response_dtos = product_use_cases.get_all_products()
return jsonify([dto.__dict__ for dto in response_dtos]), 200
@[Link]('/products/<string:product_id>', methods=['PUT'])
def update_product(product_id):
data = request.get_json()
try:
dto = ProductUpdateDTO(id=product_id, **data)
response_dto = product_use_cases.update_product(dto)
if response_dto:
return jsonify(response_dto.__dict__), 200
return jsonify({'message': 'Product not found'}), 404
except ValueError as e:
return jsonify({'error': str(e)}), 400
@[Link]('/products/<string:product_id>', methods=['DELETE'])
def delete_product(product_id):
if product_use_cases.delete_product(product_id):
return jsonify({'message': 'Product deleted successfully'}), 204
return jsonify({'message': 'Product not found'}), 404
if __name__ == '__main__':
[Link](debug=True)
Explication :
Cette couche utilise Flask pour exposer une API REST. Elle dépend des DTOs et des
cas d'utilisation de la couche application . [100]
Le
point clé ici est le Composition Root où les dépendances sont assemblées. C'est le seul
endroit où les couches externes connaissent les implémentations concrètes des couches
internes. [101] * Les vues gèrent la sérialisation/désérialisation des données JSON et
appellent les cas d'utilisation. Elles ne contiennent pas de logique métier. [102]
Le fichier [Link] est le point d'entrée de l'application. Dans un microservice Flask, il peut
simplement lancer l'application Flask définie dans [Link] .
product_microservice/[Link]
if __name__ == '__main__':
[Link](debug=True, port=5000)
Flask==2.3.2
Ce chapitre a fourni un exemple concret de microservice Python structuré selon les principes
de la Clean Architecture et SOLID. Nous avons vu comment chaque couche a une
responsabilité unique et comment les dépendances sont inversées grâce aux interfaces et à
l'injection de dépendances. Cette structure permet une grande flexibilité, une meilleure
testabilité et une maintenance facilitée du code.
Références du Chapitre 7
[94] Martin, R. C. (2017). Clean Architecture: A Craftsman's Guide to Software Structure and
Design. Pearson Education. (Chapter 22, The Clean Architecture - Entities) [95] Martin, R. C.
(n.d.). Interface segregation principle. Consulté le 28 juin 2025, de
[Link] [96] Fowler, M. (n.d.). Data
Transfer Object. Consulté le 28 juin 2025, de
[Link] [97] Martin, R. C. (n.d.).
Dependency inversion principle. Consulté le 28 juin 2025, de
[Link] [98] Martin, R. C. (2017). Clean
Architecture: A Craftsman's Guide to Software Structure and Design. Pearson Education.
(Chapter 10, Use Cases) [99] Martin, R. C. (2017). Clean Architecture: A Craftsman's Guide to
Software Structure and Design. Pearson Education. (Chapter 11, Presenters and Gateways)
[100] Martin, R. C. (2017). Clean Architecture: A Craftsman's Guide to Software Structure and
Design. Pearson Education. (Chapter 11, Presenters and Gateways) [101] Martin, R. C. (2017).
Clean Architecture: A Craftsman's Guide to Software Structure and Design. Pearson Education.
(Chapter 11, Presenters and Gateways) [102] Flask. (n.d.). Views. Consulté le 28 juin 2025, de
[Link]
Objectif
Ce chapitre récapitule les bonnes pratiques en POO Python, met en évidence l'importance des
tests dans une architecture propre, et conclut le cours en soulignant les bénéfices d'une
approche architecturale solide.
Contenu détaillé
Au-delà des principes SOLID et de la Clean Architecture, certaines bonnes pratiques générales
en POO Python contribuent à un code de haute qualité, maintenable et lisible. [103]
Les noms doivent être descriptifs et éviter les abréviations ambiguës. Un bon nom
rend le code auto-documenté. [104]
Documentation (Docstrings) :
Args:
a (int ou float): Le premier nombre.
b (int ou float): Le deuxième nombre.
Returns:
int ou float: La somme de a et b.
"""
return a + b
```
Si une dépendance circulaire apparaît, cela peut indiquer une violation du SRP ou
un besoin de refactoriser en introduisant une abstraction.
Ne retournez pas None ou des valeurs spéciales pour indiquer une erreur. Levez
des exceptions explicites. [107]
Cela rend le code plus robuste et la gestion des erreurs plus claire.
Une des motivations principales derrière la Clean Architecture et les principes SOLID est de
rendre le code plus testable. La séparation des préoccupations et l'inversion des dépendances
facilitent grandement l'écriture de tests unitaires et d'intégration. [110]
Tests Unitaires :
Ciblent les plus petites unités de code (méthodes, fonctions, classes) de manière
isolée. [111]
Dans une Clean Architecture, les entités et les cas d'utilisation sont
particulièrement faciles à tester unitairement car ils n'ont pas de dépendances
directes avec l'infrastructure ou les frameworks. Vous pouvez mocker (simuler) les
interfaces de repository ou de service. [112]
```python
response = self.use_case.create_product(dto)
[Link]([Link])
[Link]([Link], "Test Prod")
# Vérifie que la méthode add du repository a été appelée une fois
self.mock_repo.add.assert_called_once()
# Vous pouvez aussi vérifier les arguments passés à add
# self.mock_repo.add.assert_called_once_with(Product(id=..., name="Test
Prod", ...))
```
Tests d'Intégration :
Ils sont plus lents que les tests unitaires mais essentiels pour s'assurer que les
différentes parties du système fonctionnent ensemble comme prévu.
```python
def test_create_and_get_product(self):
dto = ProductCreateDTO(name="Int Test Prod", description="Desc", price=20.0,
stock=10)
created_product_dto = self.use_case.create_product(dto)
retrieved_product_dto =
self.use_case.get_product_by_id(created_product_dto.id)
[Link](retrieved_product_dto)
[Link](retrieved_product_dto.name, "Int Test Prod")
[Link](retrieved_product_dto.price, 20.0)
```
Avantages de la Testabilité :
Détection Précoce des Bugs : Les problèmes sont identifiés plus tôt dans le cycle
de développement.
Confiance dans le Code : Les tests donnent l'assurance que les modifications
n'introduisent pas de régressions.
Testabilité Supérieure : La logique métier est isolée des détails d'infrastructure, ce qui
permet des tests unitaires rapides et fiables, augmentant la confiance dans le code. [116]
Flexibilité et Évolutivité : La capacité de changer facilement les bases de données, les
frameworks UI ou d'autres détails d'implémentation sans affecter le cœur de
l'application. Le système est ouvert à l'extension de nouvelles fonctionnalités sans
nécessiter de modifications majeures du code existant. [117]
Code Réutilisable : Les entités et les cas d'utilisation, étant indépendants des détails,
peuvent être réutilisés dans différents contextes (par exemple, une API web, une
application de bureau, un script en ligne de commande). [118]
Collaboration Améliorée : Une architecture bien définie facilite le travail en équipe, car
chaque développeur peut se concentrer sur une couche ou un composant spécifique
sans interférer avec les autres.
En fin de compte, l'investissement dans une architecture propre et le respect des principes
SOLID se traduisent par des logiciels de meilleure qualité, plus durables et plus adaptables
aux besoins changeants de l'entreprise. C'est un chemin vers l'excellence logicielle qui vous
permettra de construire des systèmes robustes et performants en Python.
Références du Chapitre 8
[103] [Link]. (n.d.). PEP 8 -- Style Guide for Python Code. Consulté le 28 juin 2025, de
[Link] [104] Clean Code. (n.d.). Meaningful Names. Consulté le 28
juin 2025, de [Link] [105] [Link]. (n.d.). PEP 257 --
Docstring Conventions. Consulté le 28 juin 2025, de [Link] [106]
Real Python. (n.d.). Circular Imports in Python. Consulté le 28 juin 2025, de
[Link] [107] [Link]. (n.d.). Errors and
Exceptions. Consulté le 28 juin 2025, de [Link] [108]
Wikipedia. (n.d.). KISS principle. Consulté le 28 juin 2025, de
[Link] [109] Wikipedia. (n.d.). Don't repeat yourself.
Consulté le 28 juin 2025, de [Link] [110]
Martin, R. C. (2017). Clean Architecture: A Craftsman's Guide to Software Structure and Design.
Pearson Education. (Chapter 12, Test Boundaries) [111] Martin, R. C. (2017). Clean
Architecture: A Craftsman's Guide to Software Structure and Design. Pearson Education.
(Chapter 12, Test Boundaries) [112] Real Python. (n.d.). Getting Started With Testing in Python.
Consulté le 28 juin 2025, de [Link] [113] Martin, R. C. (2017).
Clean Architecture: A Craftsman's Guide to Software Structure and Design. Pearson Education.
(Chapter 12, Test Boundaries) [114] Real Python. (n.d.). Why You Should Write Unit Tests.
Consulté le 28 juin 2025, de [Link]
unit-tests [115] Martin, R. C. (2017). Clean Architecture: A Craftsman's Guide to Software
Structure and Design. Pearson Education. (Chapter 22, The Clean Architecture) [116] Martin, R.
C. (2017). Clean Architecture: A Craftsman's Guide to Software Structure and Design. Pearson
Education. (Chapter 22, The Clean Architecture) [117] Martin, R. C. (2017). Clean Architecture: A
Craftsman's Guide to Software Structure and Design. Pearson Education. (Chapter 22, The
Clean Architecture) [118] Martin, R. C. (2017). Clean Architecture: A Craftsman's Guide to
Software Structure and Design. Pearson Education. (Chapter 22, The Clean Architecture)