0% ont trouvé ce document utile (0 vote)
3 vues58 pages

POO Python Clean Architecture SOLID Course

Ce cours complet sur la Programmation Orientée Objet (POO) en Python aborde les concepts fondamentaux et avancés, en mettant l'accent sur la Clean Architecture et les principes SOLID pour créer des systèmes logiciels maintenables et flexibles. Les chapitres couvrent des sujets tels que l'héritage, le polymorphisme, l'encapsulation et l'abstraction, tout en fournissant des exemples pratiques. L'objectif est de fournir une compréhension approfondie de la POO en Python, permettant aux développeurs de construire des applications bien structurées.

Transféré par

deutchayvan37
Copyright
© All Rights Reserved
Nous prenons très au sérieux les droits relatifs au contenu. Si vous pensez qu’il s’agit de votre contenu, signalez une atteinte au droit d’auteur ici.
Formats disponibles
Téléchargez aux formats PDF, TXT ou lisez en ligne sur Scribd
0% ont trouvé ce document utile (0 vote)
3 vues58 pages

POO Python Clean Architecture SOLID Course

Ce cours complet sur la Programmation Orientée Objet (POO) en Python aborde les concepts fondamentaux et avancés, en mettant l'accent sur la Clean Architecture et les principes SOLID pour créer des systèmes logiciels maintenables et flexibles. Les chapitres couvrent des sujets tels que l'héritage, le polymorphisme, l'encapsulation et l'abstraction, tout en fournissant des exemples pratiques. L'objectif est de fournir une compréhension approfondie de la POO en Python, permettant aux développeurs de construire des applications bien structurées.

Transféré par

deutchayvan37
Copyright
© All Rights Reserved
Nous prenons très au sérieux les droits relatifs au contenu. Si vous pensez qu’il s’agit de votre contenu, signalez une atteinte au droit d’auteur ici.
Formats disponibles
Téléchargez aux formats PDF, TXT ou lisez en ligne sur Scribd

Cours Complet sur la Programmation

Orientée Objet (POO) en Python avec Clean


Architecture et SOLID

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.

Pourquoi la POO en Python ?

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]

Qu'est-ce que la Clean Architecture ?

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

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]

1. Single Responsibility Principle (SRP) - Principe de Responsabilité Unique : Une classe ne


devrait avoir qu'une seule raison de changer. Cela signifie qu'une classe ne devrait avoir
qu'une seule responsabilité. [4]

2. Open/Closed Principle (OCP) - Principe Ouvert/Fermé : Les entités logicielles (classes,


modules, fonctions, etc.) devraient être ouvertes à l'extension, mais fermées à la
modification. [5]

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]

4. Interface Segregation Principle (ISP) - Principe de Ségrégation des Interfaces : Un client


ne devrait pas être forcé de dépendre d'interfaces qu'il n'utilise pas. [7]

5. Dependency Inversion Principle (DIP) - Principe d'Inversion de Dépendances : Les


modules de haut niveau ne devraient pas dépendre des modules de bas niveau. Les deux
devraient dépendre d'abstractions. Les abstractions ne devraient pas dépendre des
détails. Les détails devraient dépendre des abstractions. [8]

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.

Chapitre 1 : Fondamentaux de la POO en Python

Chapitre 2 : Héritage et Polymorphisme

Chapitre 3 : Encapsulation et Abstraction


Chapitre 4 : Méthodes Spéciales (Dunder Methods) et Concepts Avancés

Chapitre 5 : Introduction à la Clean Architecture en Python

Chapitre 6 : Application des principes SOLID dans la Clean Architecture

Chapitre 7 : Exemple Concret et Bonnes Pratiques

Commençons notre voyage dans le monde de la POO en Python avec une architecture propre
et des principes solides.

Chapitre 1 : Fondamentaux de la POO en Python (Classes,


Objets, Attributs, Méthodes)

Objectif

Comprendre les concepts de base de la Programmation Orientée Objet en Python, y compris


la définition de classes, la création d'objets, la gestion des attributs et l'implémentation des
méthodes.

Contenu détaillé

1. Qu'est-ce qu'une Classe ?

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]

En Python, une classe est définie à l'aide du mot-clé class .

class MaPremiereClasse:
pass # 'pass' est utilisé comme un espace réservé pour une classe vide

2. Qu'est-ce qu'un Objet ?

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]

```python class Voiture: roues = 4 # Attribut de classe

voiture1 = Voiture() voiture2 = Voiture()

print([Link]) # Output: 4 print([Link]) # Output: 4

[Link] = 5 # Modifier l'attribut de classe affecte toutes les instances


print([Link]) # Output: 5 ```

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

personne1 = Personne("Alice", 30) personne2 = Personne("Bob", 25)

print([Link], [Link]) # Output: Alice 30 print([Link],


[Link]) # Output: Bob 25

[Link] = 31 # Modifier l'attribut d'instance n'affecte que cette instance


print([Link]) # Output: 31 print([Link]) # Output: 25 ```

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]}."

mon_chien = Chien("Buddy", "Golden Retriever") print(mon_chien.aboyer()) # Output:


Buddy le Golden Retriever aboie ! print(mon_chien.decrire()) # Output: Je suis Buddy, un
Golden Retriever. ```

La méthode __init__ (Constructeur) : C'est une méthode spéciale (appelée

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}."

livre1 = Livre("Le Seigneur des Anneaux", "J.R.R. Tolkien", 1954)


print(livre1.get_info())
```

Méthodes de classe ( @classmethod ) : Ces méthodes opèrent sur la classe elle-même


plutôt que sur une instance spécifique. Elles prennent cls (conventionnellement)
comme premier argument, qui fait référence à la classe. Elles sont souvent utilisées
comme constructeurs alternatifs ou pour des opérations qui n'ont pas besoin d'une
instance. [16]

```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]}"

date1 = Date(28, 6, 2025) print(date1.afficher_date()) # Output: 28-06-2025


date2 = Date.from_string("01-01-2024") print(date2.afficher_date()) # Output: 01-01-
2024 ```

Méthodes statiques ( @staticmethod ) : Ces méthodes n'opèrent ni sur l'instance


( self ) ni sur la classe ( cls ). Elles sont similaires à des fonctions régulières, mais sont
logiquement regroupées avec une classe car elles ont un rapport conceptuel avec elle.
Elles ne peuvent pas modifier l'état de l'objet ou de la classe. [17]

```python class UtilitaireMath: @staticmethod def ajouter(a, b): return a + b

@staticmethod
def multiplier(a, b):
return a * b

print([Link](5, 3)) # Output: 8 print([Link](4, 2)) #


Output: 8 ```

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

[9] [Link]. (n.d.). Classes. Consulté le 28 juin 2025, de


[Link] [10] [Link]. (n.d.). Classes. Consulté le 28
juin 2025, de [Link] [11] Real Python. (n.d.). Python
Class Attributes vs. Instance Attributes. Consulté le 28 juin 2025, de
[Link] [12] Real Python.
(n.d.). Python Class Attributes vs. Instance Attributes. Consulté le 28 juin 2025, de
[Link] [13] GeeksforGeeks.
(n.d.). Classes and Objects in Python. Consulté le 28 juin 2025, de
[Link] [14] GeeksforGeeks. (n.d.).
Classes and Objects in Python. Consulté le 28 juin 2025, de
[Link] [15] [Link]. (n.d.). Classes.
Consulté le 28 juin 2025, de [Link] [16] Real Python.
(n.d.). Python @classmethod and @staticmethod Explained. Consulté le 28 juin 2025, de
[Link] [17] Real Python.
(n.d.). Python @classmethod and @staticmethod Explained. Consulté le 28 juin 2025, de
[Link]

Chapitre 2 : Héritage et Polymorphisme avec des exemples


SOLID

Objectif

Approfondir les concepts d'héritage et de polymorphisme en Python, et comprendre


comment ces piliers de la POO sont renforcés par les principes SOLID, notamment le Principe
de Substitution de Liskov (LSP) et le Principe Ouvert/Fermé (OCP).

Contenu détaillé

1. L'Héritage : Réutilisation du Code

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

« est un type de » entre les classes. [18]

Syntaxe de l'héritage en Python :


class Animal:
def __init__(self, nom):
[Link] = nom

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."

mon_chien = Chien("Buddy", "Golden Retriever")


mon_chat = Chat("Whiskers")

print(mon_chien.faire_son()) # Output: Buddy aboie.


print(mon_chat.faire_son()) # Output: Whiskers miaule.
print(mon_chien.nom) # Output: Buddy

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]

2. Le Polymorphisme : Une Interface, Plusieurs Formes

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())

interagir_avec_animal(mon_chien) # Output: Buddy aboie.


interagir_avec_animal(mon_chat) # Output: Whiskers miaule.

# 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]

3. Principe de Substitution de Liskov (LSP)

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]

Violation du LSP : Considérons un exemple classique de violation du LSP avec un carré et un


rectangle.
class Rectangle:
def __init__(self, largeur, hauteur):
self._largeur = largeur
self._hauteur = hauteur

@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)

augmenter_largeur(rect) # Output: Largeur: 10, Hauteur: 4, Aire: 40


augmenter_largeur(carre) # Output: Largeur: 10, Hauteur: 10, Aire: 100 (comportement
inattendu pour un 'Rectangle')

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]

4. Principe Ouvert/Fermé (OCP)

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]

Violation de l'OCP : Considérons un système de calcul de salaire où de nouveaux types


d'employés sont ajoutés.

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

# Violation: chaque nouveau type d'employé nécessite une modification de


CalculateurSalaire

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

Ce chapitre a exploré l'héritage et le polymorphisme, deux piliers fondamentaux de la POO.


Nous avons également vu comment les principes SOLID, en particulier le Principe de
Substitution de Liskov (LSP) et le Principe Ouvert/Fermé (OCP), guident l'utilisation correcte
de l'héritage et du polymorphisme pour créer des systèmes flexibles, robustes et faciles à
étendre.

Références du Chapitre 2

[18] [Link]. (n.d.). Inheritance. Consulté le 28 juin 2025, de


[Link] [19] GeeksforGeeks. (n.d.).
Inheritance in Python. Consulté le 28 juin 2025, de
[Link] [20] GeeksforGeeks. (n.d.).
Polymorphism in Python. Consulté le 28 juin 2025, de
[Link] [21] Real Python. (n.d.). Python
Polymorphism. Consulté le 28 juin 2025, de [Link]
[22] Martin, R. C. (n.d.). Liskov substitution principle. Consulté le 28 juin 2025, de
[Link] [23] Stack Overflow. (n.d.). Why is
the Liskov Substitution Principle important?. Consulté le 28 juin 2025, de
[Link]
important [24] GeeksforGeeks. (n.d.). Liskov Substitution Principle in Python. Consulté le 28
juin 2025, de [Link] [25]
Martin, R. C. (n.d.). Open/closed principle. Consulté le 28 juin 2025, de
[Link] [26] GeeksforGeeks. (n.d.). Open-Closed
Principle in Python. Consulté le 28 juin 2025, de [Link]
principle-in-python/ [27] Real Python. (n.d.). The Open/Closed Principle in Python. Consulté le
28 juin 2025, de [Link]

Chapitre 3 : Encapsulation et Abstraction (Principes SOLID)

Objectif

Comprendre les concepts d'encapsulation et d'abstraction en POO Python, et comment ils


sont liés aux principes SOLID, en particulier le Principe de Responsabilité Unique (SRP) et le
Principe de Ségrégation des Interfaces (ISP).

Contenu détaillé

1. Encapsulation : Protéger les Données et Contrôler l'Accès

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]

En Python, l'encapsulation est principalement mise en œuvre par convention et par


l'utilisation de propriétés (getters et setters). Python n'a pas de mots-clés public , private
ou protected stricts comme Java ou C++. [29]

Conventions de nommage pour l'encapsulation :


Attributs publics : nom_attribut . Accessibles de partout.

Attributs "protégés" (par convention) : _nom_attribut . Indique aux


développeurs que cet attribut est destiné à un usage interne à la classe ou à ses
sous-classes, mais il reste techniquement accessible de l'extérieur. C'est une
convention, pas une restriction. [30]

Attributs "privés" (par convention/mangling de nom) : __nom_attribut .


Python effectue un "name mangling" pour ces attributs, ce qui rend leur accès
direct de l'extérieur plus difficile (mais pas impossible). Le nom de l'attribut est
transformé en _NomDeLaClasse__nom_attribut . [31]

Exemple d'encapsulation avec conventions :

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 deposer(self, montant):


if montant > 0:
self._solde += montant
print(f"Dépôt de {montant}€. Nouveau solde : {self._solde}€.")
else:
print("Le montant du dépôt doit être positif.")

def retirer(self, montant):


if 0 < montant <= self._solde:
self._solde -= montant
print(f"Retrait de {montant}€. Nouveau solde : {self._solde}€.")
else:
print("Fonds insuffisants ou montant invalide.")

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()}€.")

# Accès direct (déconseillé pour _solde, plus difficile pour __numero_compte)


print(mon_compte._solde) # Accès possible, mais conventionnellement à éviter
# print(mon_compte.__numero_compte) # Erreur: AttributeError
print(mon_compte._CompteBancaire__numero_compte) # Accès possible via name mangling

2. Propriétés ( @property ) : L'Encapsulation à la Python

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

[Link] = 7 # Accès via le setter


print(f"Nouveau rayon : {[Link]}")
print(f"Nouvelle aire : {[Link]}")

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)

Les propriétés permettent de masquer la complexité interne et de présenter une interface


simple et cohérente aux utilisateurs de la classe. [33]

3. Abstraction : Se Concentrer sur l'Essentiel

L'abstraction est le processus de simplification de systèmes complexes en modélisant des


classes basées sur des aspects essentiels, en ignorant les détails non pertinents. Elle se
concentre sur "ce que" fait un objet plutôt que sur "comment" il le fait. L'abstraction est
étroitement liée à l'encapsulation, car l'encapsulation est le mécanisme par lequel
l'abstraction est réalisée. [34]

En Python, l'abstraction est souvent mise en œuvre à l'aide de classes abstraites et de


méthodes abstraites, qui sont définies à l'aide du module abc (Abstract Base Classes). Une
classe abstraite ne peut pas être instanciée directement et est destinée à être héritée par
d'autres classes. Une méthode abstraite doit être implémentée par toutes les sous-classes
concrètes. [35]

from abc import ABC, abstractmethod

class Forme(ABC): # Classe abstraite


@abstractmethod
def aire(self):
pass # Méthode abstraite, doit être implémentée par les sous-classes

@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.

from abc import ABC, abstractmethod

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.")

# Violation: Developpeur est forcé d'implémenter gerer_equipe qu'il n'utilise pas.

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.

from abc import ABC, abstractmethod

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

class Developpeur(TravailleurGeneral, Codeur):


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 coder(self):
print("Le développeur code.")

class ChefDeProjet(TravailleurGeneral, Gerant):


def travailler(self):
print("Le chef de projet travaille.")

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

Ce chapitre a exploré l'encapsulation et l'abstraction, des concepts clés pour la conception de


classes robustes et modulaires. Nous avons vu comment Python gère l'encapsulation via des
conventions et des propriétés, et comment l'abstraction est mise en œuvre avec les classes
abstraites. Enfin, nous avons examiné le Principe de Ségrégation des Interfaces (ISP) et son
importance pour créer des interfaces spécifiques au client, évitant ainsi des dépendances
inutiles.

Références du Chapitre 3

[28] GeeksforGeeks. (n.d.). Encapsulation in Python. Consulté le 28 juin 2025, de


[Link] [29] Real Python. (n.d.). Python
@property: The Smart Way to Define Attributes. Consulté le 28 juin 2025, de
[Link] [30] [Link]. (n.d.). Classes. Consulté le 28 juin
2025, de [Link] [31] [Link].
(n.d.). Classes. Consulté le 28 juin 2025, de
[Link] [32] Real Python. (n.d.).
Python @property: The Smart Way to Define Attributes. Consulté le 28 juin 2025, de
[Link] [33] GeeksforGeeks. (n.d.). Python @property
decorator. Consulté le 28 juin 2025, de [Link]
decorator/ [34] GeeksforGeeks. (n.d.). Abstraction in Python. Consulté le 28 juin 2025, de
[Link] [35] [Link]. (n.d.). abc — Abstract
Base Classes. Consulté le 28 juin 2025, de [Link] [36]
GeeksforGeeks. (n.d.). Abstract Classes in Python. Consulté le 28 juin 2025, de
[Link] [37] Martin, R. C. (n.d.). Interface
segregation principle. Consulté le 28 juin 2025, de
[Link] [38] GeeksforGeeks. (n.d.).
Interface Segregation Principle in Python. Consulté le 28 juin 2025, de
[Link] [39] Real Python.
(n.d.). The Interface Segregation Principle in Python. Consulté le 28 juin 2025, de
[Link]
Chapitre 4 : Méthodes Spéciales (Dunder Methods) et
Concepts Avancés

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é

1. Méthodes Spéciales (Dunder Methods)

Les méthodes spéciales, également appelées "méthodes magiques" ou "dunder methods"


(pour double underscore), sont des méthodes en Python qui ont des noms commençant et se
terminant par deux underscores (par exemple, __init__ , __str__ , __add__ ). Elles
permettent d'implémenter un comportement d'objet qui interagit avec les opérations
intégrées du langage, telles que la création d'objets, la représentation textuelle, les
comparaisons, les opérations arithmétiques, et bien d'autres. [40]

L'utilisation des dunder methods est au cœur de la surcharge d'opérateurs (operator


overloading) en Python, permettant à vos objets personnalisés de se comporter comme des
types de données intégrés. [41]

Voici quelques-unes des dunder methods les plus courantes et leur utilité :

__init__(self, ...) : Le constructeur de la classe. Appelé lors de la création d'une


nouvelle instance de la classe. Nous l'avons déjà vu en détail. [42]

__str__(self) : Définit la représentation "informelle" ou "lisible par l'utilisateur" d'un


objet. C'est ce qui est retourné lorsque vous utilisez print() ou str() sur un objet. [43]

__repr__(self) : Définit la représentation "officielle" ou "non ambiguë" d'un objet. Elle


est destinée aux développeurs et devrait, si possible, retourner une chaîne qui,
lorsqu'elle est évaluée, recrée l'objet. C'est ce qui est retourné lorsque vous tapez le nom
de l'objet dans l'interpréteur Python ou utilisez repr() . [44]

```python class Point: def init(self, x, y): self.x = x self.y = y


def __str__(self):
return f"Point({self.x}, {self.y})"

def __repr__(self):
return f"Point(x={self.x}, y={self.y})"

p = Point(1, 2) print(p) # Output: Point(1, 2) (utilise str) print(repr(p)) # Output: Point(x=1,


y=2) (utilise repr) ```

__len__(self) : Définit le comportement de la fonction len() pour les objets de la


classe. [45]

```python class MaCollection: def init(self, elements): [Link] = elements

def __len__(self):
return len([Link])

mc = MaCollection([1, 2, 3, 4, 5]) print(len(mc)) # Output: 5 ```

__add__(self, other) , __sub__(self, other) , etc. : Définissent le comportement


des opérateurs arithmétiques ( + , - , etc.). [46]

```python class Vecteur: def init(self, x, y): self.x = x self.y = y

def __add__(self, other):


return Vecteur(self.x + other.x, self.y + other.y)

def __str__(self):
return f"Vecteur({self.x}, {self.y})"

v1 = Vecteur(1, 2) v2 = Vecteur(3, 4) v3 = v1 + v2 print(v3) # Output: Vecteur(4, 6) ```

__eq__(self, other) , __ne__(self, other) , etc. : Définissent le comportement des


opérateurs de comparaison ( == , != , etc.). [47]

```python class Personne: def init(self, nom, age): [Link] = nom [Link] = age

def __eq__(self, other):


if not isinstance(other, Personne):
return NotImplemented
return [Link] == [Link] and [Link] == [Link]

p1 = Personne("Alice", 30) p2 = Personne("Alice", 30) p3 = Personne("Bob", 25)

print(p1 == p2) # Output: True print(p1 == p3) # Output: False ```

__getitem__(self, key) , __setitem__(self, key, value) , __delitem__(self,


key) : Permettent aux objets de se comporter comme des dictionnaires ou des listes
(accès par index/clé). [48]

```python class MonDictionnaire: def init(self): self._data = {}

def __getitem__(self, key):


return self._data[key]

def __setitem__(self, key, value):


self._data[key] = value

def __delitem__(self, key):


del self._data[key]

md = MonDictionnaire() md["cle1"] = "valeur1" print(md["cle1"]) # Output: valeur1 del


md["cle1"]

print(md["cle1"]) # Erreur: KeyError


```

L'utilisation judicieuse des dunder methods peut rendre vos classes plus intuitives et plus
"pythoniques".

2. Composition vs. Héritage

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]

3. Design Patterns Simples

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]

Voici quelques design patterns simples et très utiles en Python :

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]

```python class Singleton: _instance = None


def __new__(cls):
if cls._instance is None:
cls._instance = super(Singleton, cls).__new__(cls)
return cls._instance

s1 = Singleton() s2 = Singleton() print(s1 is s2) # Output: True ```

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]

```python from abc import ABC, abstractmethod

class Produit(ABC): @abstractmethod def operation(self): pass

class ProduitA(Produit): def operation(self): return "Opération de ProduitA"

class ProduitB(Produit): def operation(self): return "Opération de ProduitB"

class Createur(ABC): @abstractmethod def factory_method(self): pass

def une_operation(self):
produit = self.factory_method()
result = f"Createur: {[Link]()}"
return result

class CreateurA(Createur): def factory_method(self): return ProduitA()

class CreateurB(Createur): def factory_method(self): return ProduitB()

createur_a = CreateurA() print(createur_a.une_operation()) # Output: Createur:


Opération de ProduitA

createur_b = CreateurB() print(createur_b.une_operation()) # Output: Createur:


Opération de ProduitB ```

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]

```python class Sujet: def init(self): self._observateurs = []


def attacher(self, observateur):
self._observateurs.append(observateur)

def detacher(self, observateur):


self._observateurs.remove(observateur)

def notifier(self, message):


for observateur in self._observateurs:
observateur.mise_a_jour(message)

class Observateur: def init(self, nom): [Link] = nom

def mise_a_jour(self, message):


print(f"{[Link]} a reçu le message: {message}")

sujet = Sujet() obs1 = Observateur("Observateur 1") obs2 = Observateur("Observateur 2")

[Link](obs1) [Link](obs2)

[Link]("Premier événement !")

Output:

Observateur 1 a reçu le message: Premier


événement !

Observateur 2 a reçu le message: Premier


événement !
[Link](obs2) [Link]("Deuxième événement !")
Output:

Observateur 1 a reçu le message:


Deuxième événement !
```

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

Ce chapitre a approfondi les capacités de la POO en Python en explorant les méthodes


spéciales (dunder methods) qui permettent de personnaliser le comportement des objets
pour les opérations intégrées du langage. Nous avons également discuté de l'importance de la
composition par rapport à l'héritage pour des designs plus flexibles, et introduit quelques
design patterns fondamentaux qui peuvent améliorer la structure et la maintenabilité de votre
code.

Références du Chapitre 4

[40] [Link]. (n.d.). Data model. Consulté le 28 juin 2025, de


[Link] [41] Real
Python. (n.d.). Python's __init__ and __repr__ . Consulté le 28 juin 2025, de
[Link] [42] [Link]. (n.d.). Data model. Consulté le 28
juin 2025, de [Link] [43]
[Link]. (n.d.). Data model. Consulté le 28 juin 2025, de
[Link] [44] [Link]. (n.d.). Data
model. Consulté le 28 juin 2025, de
[Link] [45] [Link]. (n.d.). Data
model. Consulté le 28 juin 2025, de
[Link] [46] [Link]. (n.d.). Data
model. Consulté le 28 juin 2025, de
[Link] [47]
[Link]. (n.d.). Data model. Consulté le 28 juin 2025, de
[Link] [48] [Link]. (n.d.). Data
model. Consulté le 28 juin 2025, de
[Link] [49]
GeeksforGeeks. (n.d.). Composition vs Inheritance in Python. Consulté le 28 juin 2025, de
[Link] [50] Wikipedia. (n.d.).
Composition over inheritance. Consulté le 28 juin 2025, de
[Link] [51] [Link]. (n.d.).
Design Patterns. Consulté le 28 juin 2025, de [Link] [52]
[Link]. (n.d.). Singleton. Consulté le 28 juin 2025, de
[Link] [53] [Link]. (n.d.). Factory
Method. Consulté le 28 juin 2025, de [Link]
[54] [Link]. (n.d.). Observer. Consulté le 28 juin 2025, de
[Link]

Chapitre 5 : Introduction à la Clean Architecture en Python


(Couches)

Objectif

Comprendre les principes fondamentaux de la Clean Architecture et comment elle s'applique


à la conception d'applications en Python, en mettant l'accent sur la séparation des
préoccupations et l'indépendance des frameworks.

Contenu détaillé

1. Les Problèmes de l'Architecture Traditionnelle

Dans de nombreuses architectures logicielles traditionnelles, la logique métier est souvent


étroitement liée aux détails d'implémentation tels que la base de données, l'interface
utilisateur ou les frameworks web. Cette approche, bien que simple pour de petites
applications, conduit rapidement à des problèmes de maintenabilité, de testabilité et de
flexibilité à mesure que l'application grandit. [55]

Couplage Fort : Les changements dans la base de données ou le framework peuvent


nécessiter des modifications importantes dans la logique métier.

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.

Dépendance aux Détails : Le cœur de l'application dépend des détails, ce qui va à


l'encontre de l'idée que les règles métier devraient être les plus stables. [56]
2. Les Principes Fondamentaux de la Clean Architecture

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]

Cas d'Utilisation (Use Cases) : Contiennent la logique métier spécifique à l'application.


Ils orchestrent le flux de données vers et depuis les entités, et dirigent les entités pour
qu'elles accomplissent leurs objectifs. Ils ne sont pas affectés par les changements de
l'interface utilisateur, de la base de données ou des frameworks externes. [59]

Adaptateurs d'Interface (Interface Adapters) : Convertissent les données des formats


les plus externes (base de données, web, etc.) vers les formats des couches internes
(entités, cas d'utilisation) et vice-versa. Cette couche inclut les Presenters, Gateways, et
Controllers. C'est ici que les frameworks et les bases de données sont adaptés pour
interagir avec les couches internes. [60]

Frameworks et Pilotes (Frameworks & Drivers) : La couche la plus externe. Elle


contient les frameworks web (Django, Flask), les bases de données (SQLAlchemy, ORM),
les outils, etc. Ces éléments sont des détails d'implémentation et ne devraient pas
influencer les couches internes. [61]

Diagramme de la Clean Architecture

Diagramme de la Clean Architecture (Source: Wikipedia)

La Règle de Dépendance (Dependency Rule) est le principe le plus important de la Clean


Architecture : les dépendances du code source ne peuvent pointer que vers l'intérieur. Aucune
entité dans un cercle intérieur ne peut connaître quoi que ce soit des entités dans un cercle
extérieur. Cela signifie que les entités ne connaissent pas les cas d'utilisation, les cas
d'utilisation ne connaissent pas les adaptateurs, et les adaptateurs ne connaissent pas les
frameworks. [62]

3. Application de la Clean Architecture en Python

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)

Explication des Couches en Python :

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]

application/ (Cas d'Utilisation) : Contient les classes qui encapsulent la logique


métier de l'application. Elles dépendent des entités de la couche core et des interfaces
définies dans core/[Link] . Elles reçoivent des DTOs en entrée et retournent
des DTOs en sortie. Elles ne doivent pas connaître les détails de l'infrastructure ou de la
présentation. [65]

infrastructure/ (Adaptateurs d'Interface) : C'est la couche où les interfaces définies


dans core et application sont implémentées. Par exemple, si core/[Link]
définit IUserRepository , alors
infrastructure/repositories/sqlalchemy_user_repository.py ou
infrastructure/repositories/django_user_repository.py implémentera cette
interface. Cette couche contient les détails techniques (accès à la base de données,
appels API externes, etc.). [66]

presentation/ (Frameworks et Pilotes) : La couche la plus externe. Elle est


responsable de l'interaction avec l'utilisateur ou d'autres systèmes. Elle utilise les cas
d'utilisation de la couche application pour exécuter les opérations métier. C'est ici que
les frameworks web (Django, Flask, FastAPI) sont configurés et que les points d'entrée
(vues, contrôleurs) sont définis. [67]

4. La Règle de Dépendance en Pratique (Python)

Pour faire respecter la règle de dépendance, nous utilisons l'injection de dépendances. Au


lieu qu'une classe crée ses propres dépendances, celles-ci lui sont fournies (injectées) lors de
son initialisation. [68]

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

def execute(self, user_id: str):


return self.user_repository.get_user_by_id(user_id)

# infrastructure/repositories/in_memory_user_repository.py
from [Link] import IUserRepository

class InMemoryUserRepository(IUserRepository):
def __init__(self):
self._users = {}

def get_user_by_id(self, user_id: str):


return self._users.get(user_id)

def save_user(self, user):


self._users[[Link]] = user

# [Link] (Composition Root)


from application.use_cases import GetUserUseCase
from [Link].in_memory_user_repository import
InMemoryUserRepository

# Ici, nous assemblons les dépendances


user_repo = InMemoryUserRepository()
get_user_use_case = GetUserUseCase(user_repository=user_repo) # Injection de
l'implémentation

# 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

Ce chapitre a introduit les concepts fondamentaux de la Clean Architecture, en expliquant les


problèmes qu'elle résout et ses couches principales. Nous avons vu comment cette
architecture peut être appliquée en Python, en utilisant une structure de répertoires claire et
en respectant la règle de dépendance grâce à l'injection de dépendances. Cette approche jette
les bases pour construire des applications Python robustes, testables et évolutives, qui seront
explorées plus en détail dans les chapitres suivants avec l'intégration des principes SOLID.

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]

Chapitre 6 : Application des principes SOLID dans la Clean


Architecture

Objectif

Comprendre comment les principes SOLID (Single Responsibility Principle, Open/Closed


Principle, Liskov Substitution Principle, Interface Segregation Principle, Dependency Inversion
Principle) sont concrètement appliqués et renforcés au sein d'une architecture logicielle
basée sur la Clean Architecture.

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]

1. Principe de Responsabilité Unique (SRP) et Clean Architecture

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]

Dans la Couche Entités ( core/[Link] ) :

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]

Dans la Couche Cas d'Utilisation ( application/use_cases.py ) :

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]

Dans la Couche Adaptateurs d'Interface ( infrastructure/ ) :

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]

Les contrôleurs/vues dans la couche de présentation ont pour seule responsabilité


de gérer les requêtes HTTP et de déléguer aux cas d'utilisation. [75]

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

def execute(self, articles_dto):


# Logique d'orchestration pour placer une commande
commande = Commande.creer_depuis_dto(articles_dto) # Supposons une méthode de
fabrique
commande.valider_commande() # La validation est la responsabilité de l'entité
self.commande_repo.save(commande) # La persistance est la responsabilité du
repository
return [Link]

# 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]

2. Principe Ouvert/Fermé (OCP) et Clean Architecture

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]

Extension sans Modification :


Lorsque vous ajoutez une nouvelle fonctionnalité, vous devriez idéalement ajouter
de nouvelles classes ou de nouveaux modules plutôt que de modifier des classes
existantes, surtout celles des couches internes. [78]

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

def execute(self, data):


[Link].log_info(f"Traitement des données: {data}")
# Logique de traitement
if not data:
[Link].log_error("Données vides reçues.")
raise ValueError("Données ne peuvent pas être vides.")
return "Données traitées."

# infrastructure/console_logger.py
from [Link] import ILogger

class ConsoleLogger(ILogger):
def log_info(self, message: str):
print(f"[INFO] {message}")

def log_error(self, message: str):


print(f"[ERREUR] {message}")

# infrastructure/file_logger.py (Nouvelle extension sans modifier


TraiterDonneesUseCase)
from [Link] import ILogger

class FileLogger(ILogger):
def __init__(self, filename="[Link]"):
[Link] = filename

def log_info(self, message: str):


with open([Link], "a") as f:
[Link](f"[INFO] {message}\n")

def log_error(self, message: str):


with open([Link], "a") as f:
[Link](f"[ERREUR] {message}\n")

# [Link] (Composition Root)


# Utilisation avec ConsoleLogger
logger_console = ConsoleLogger()
use_case_console = TraiterDonneesUseCase(logger=logger_console)
use_case_console.execute("Test data")

# Utilisation avec FileLogger (extension)


logger_file = FileLogger("mon_log.log")
use_case_file = TraiterDonneesUseCase(logger=logger_file)
use_case_file.execute("Autre test data")
Le TraiterDonneesUseCase est fermé à la modification, mais le système est ouvert à
l'extension de nouvelles implémentations de ILogger . [80]

3. Principe de Substitution de Liskov (LSP) et Clean Architecture

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]

Si un cas d'utilisation dépend de IUserRepository , il doit pouvoir fonctionner


indifféremment avec n'importe quelle implémentation concrète de
IUserRepository sans que le comportement du cas d'utilisation ne change de
manière inattendue. [83]

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

# infrastructure/repositories/in_memory_user_repository.py (déjà défini)


# class InMemoryUserRepository(IUserRepository):
# ...

# infrastructure/repositories/database_user_repository.py (Nouvelle implémentation)


# from database_orm import UserModel # Supposons un modèle ORM

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

def save_user(self, user):


# Logique pour sauvegarder l'utilisateur dans la base de données
# UserModel.create_from_entity(user).save()
pass

# application/use_cases.py (déjà défini)


# class GetUserUseCase:
# def __init__(self, user_repository: IUserRepository):
# self.user_repository = user_repository
# def execute(self, user_id: str):
# return self.user_repository.get_user_by_id(user_id)

# [Link] (Composition Root)


from application.use_cases import GetUserUseCase
from [Link].in_memory_user_repository import
InMemoryUserRepository
# from [Link].database_user_repository import
DatabaseUserRepository # Pour l'exemple

# Le cas d'utilisation fonctionne avec n'importe quelle implémentation conforme au LSP


repo_in_memory = InMemoryUserRepository()
get_user_uc_mem = GetUserUseCase(user_repository=repo_in_memory)

# 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]

4. Principe de Ségrégation des Interfaces (ISP) et Clean Architecture

Le Principe de Ségrégation des Interfaces (ISP) préconise d'avoir de nombreuses interfaces


spécifiques au client plutôt qu'une seule interface générale. Dans la Clean Architecture, cela se
manifeste par la création d'interfaces granulaires pour les besoins spécifiques de chaque cas
d'utilisation ou entité. [85]
Interfaces Granulaires :
Au lieu d'une seule interface IRepository géante avec des méthodes pour toutes
les entités, vous aurez IUserRepository , IProductRepository ,
IOrderRepository , etc. [86]

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

def execute(self, user_data):


# ... logique d'enregistrement de l'utilisateur ...
self.email_sender.send_email(user_data.email, "Bienvenue", "Merci de vous être
inscrit !")
# ...

# 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

# [Link] (Composition Root)


email_sender = SmtpEmailSender()
# sms_sender = TwilioSMSSender()

# Le cas d'utilisation EnregistrerUtilisateurUseCase ne dépend que de IEmailSender


enregistrer_uc = EnregistrerUtilisateurUseCase(user_repo=None,
email_sender=email_sender)

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]

5. Principe d'Inversion de Dépendances (DIP) et Clean Architecture

Le Principe d'Inversion de Dépendances (DIP) est le principe le plus fondamental pour la


Clean Architecture. Il stipule que : 1. Les modules de haut niveau ne devraient pas dépendre
des modules de bas niveau. Les deux devraient dépendre d'abstractions. 2. Les abstractions
ne devraient pas dépendre des détails. Les détails devraient dépendre des abstractions. [89]
Dépendance aux Abstractions :
Dans la Clean Architecture, les couches internes (Entités, Cas d'Utilisation) sont les
modules de haut niveau. Elles définissent les abstractions (interfaces) dont elles
ont besoin. [90]

Les couches externes (Adaptateurs d'Interface, Frameworks et Pilotes) sont les


modules de bas niveau. Elles fournissent les implémentations concrètes de ces
abstractions. [91]

La dépendance est "inversée" : au lieu que les cas d'utilisation dépendent


directement d'une implémentation de base de données (détail de bas niveau), ils
dépendent d'une interface de repository (abstraction de haut niveau).
L'implémentation de la base de données dépend de cette même interface. [92]

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

# application/use_cases.py (Module de haut niveau dépendant de l'abstraction)


# from [Link] import IUserRepository
# class CreerUtilisateurUseCase:
# def __init__(self, user_repository: IUserRepository):
# self.user_repository = user_repository
# def execute(self, user_data):
# # ...
# self.user_repository.save(user)

# infrastructure/repositories/database_user_repository.py (Module de bas niveau


dépendant de l'abstraction)
# from [Link] import IUserRepository
# class DatabaseUserRepository(IUserRepository):
# def save_user(self, user):
# # Implémentation concrète de la sauvegarde en base de données
# pass

# [Link] (Composition Root)


# user_repo_impl = DatabaseUserRepository() # Détail de bas niveau
# creer_user_uc = CreerUtilisateurUseCase(user_repository=user_repo_impl) # Injection
de dépendance

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]

Chapitre 7 : Exemple Concret de Microservice Python (POO,


Clean Architecture, SOLID)

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.

Contexte du Microservice : Gestion de Produits

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

La structure du projet suivra les couches de la Clean Architecture :


product_microservice/
├── core/ # Couche Entités et Interfaces (Domaine)
│ ├── [Link] # Définition de l'entité Produit
│ └── [Link] # Interfaces pour les repositories
├── application/ # Couche Cas d'Utilisation
│ ├── use_cases.py # Logique métier (créer, récupérer, etc.)
│ └── [Link] # Data Transfer Objects (DTOs)
├── infrastructure/ # Couche Adaptateurs d'Interface (Implémentations
concrètes)
│ └── repositories/ # Implémentation du repository en mémoire
├── presentation/ # Couche Frameworks et Pilotes (API)
│ └── [Link] # API REST avec Flask (ou FastAPI)
├── tests/ # Tests unitaires et d'intégration
│ ├── unit/
│ └── integration/
├── [Link] # Point d'entrée de l'application
└── [Link] # Dépendances du projet

Implémentation des Couches

7.1. Couche core (Entités et Interfaces)

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

def decrease_stock(self, quantity: int):


if quantity <= 0: raise ValueError("Quantity must be positive")
if [Link] < quantity: raise ValueError("Insufficient stock")
[Link] -= quantity

def increase_stock(self, quantity: int):


if quantity <= 0: raise ValueError("Quantity must be positive")
[Link] += quantity

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]

Elle n'a aucune dépendance externe, respectant ainsi le SRP et le DIP.

Les méthodes update_info , decrease_stock , increase_stock encapsulent la


logique métier liée au produit.

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 interface permet à la couche application de dépendre d'une abstraction


plutôt que d'une implémentation concrète, respectant le DIP.

7.2. Couche application (Cas d'Utilisation et DTOs)

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]

Ils respectent le SRP en ayant pour seule responsabilité le transport de données.

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

def create_product(self, dto: ProductCreateDTO) -> ProductResponseDTO:


product = [Link](
name=[Link],
description=[Link],
price=[Link],
stock=[Link]
)
self.product_repo.add(product)
return ProductResponseDTO(
id=[Link],
name=[Link],
description=[Link],
price=[Link],
stock=[Link]
)

def get_product_by_id(self, product_id: str) -> Optional[ProductResponseDTO]:


product = self.product_repo.get_by_id(product_id)
if product:
return ProductResponseDTO(
id=[Link],
name=[Link],
description=[Link],
price=[Link],
stock=[Link]
)
return None

def get_all_products(self) -> List[ProductResponseDTO]:


products = self.product_repo.get_all()
return [
ProductResponseDTO(
id=[Link],
name=[Link],
description=[Link],
price=[Link],
stock=[Link]
) for p in products
]

def update_product(self, dto: ProductUpdateDTO) -> Optional[ProductResponseDTO]:


product = self.product_repo.get_by_id([Link])
if not product:
return None

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]
)

def delete_product(self, product_id: str) -> bool:


product = self.product_repo.get_by_id(product_id)
if product:
self.product_repo.delete(product_id)
return True
return False

Explication :
ProductUseCases contient la logique métier de l'application. Il dépend de
IProductRepository (abstraction), respectant le DIP. [97]

Chaque méthode représente un cas d'utilisation spécifique (créer, obtenir, etc.),


respectant le SRP. [98]

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.

7.3. Couche infrastructure (Implémentations de Repository)

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

from typing import List, Optional


from [Link] import Product
from [Link] import IProductRepository

class InMemoryProductRepository(IProductRepository):
def __init__(self):
self._products = {}

def add(self, product: Product) -> None:


self._products[[Link]] = product

def get_by_id(self, product_id: str) -> Optional[Product]:


return self._products.get(product_id)

def get_all(self) -> List[Product]:


return list(self._products.values())

def update(self, product: Product) -> None:


if [Link] not in self._products:
raise ValueError(f"Product with ID {[Link]} not found for update.")
self._products[[Link]] = product

def delete(self, product_id: str) -> None:


if product_id in self._products:
del self._products[product_id]

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 .

7.4. Couche presentation (API REST avec Flask)

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__)

# Composition Root: Assemblage des dépendances


product_repository = InMemoryProductRepository()
product_use_cases = ProductUseCases(product_repo=product_repository)

@[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]

7.5. Fichier [Link]

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]

from [Link] import app

if __name__ == '__main__':
[Link](debug=True, port=5000)

7.6. Fichier [Link]

Flask==2.3.2

7.7. Résumé du Chapitre 7

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]

Chapitre 8 : Bonnes Pratiques, Tests et Conclusion

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é

8.1. Bonnes Pratiques en POO Python

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]

Nommage Clair et Cohérent :

Utilisez des noms de classes en CamelCase (ex: MaClasse ).

Utilisez des noms de fonctions et de variables en snake_case (ex: ma_fonction ,


ma_variable ).

Les noms doivent être descriptifs et éviter les abréviations ambiguës. Un bon nom
rend le code auto-documenté. [104]

Documentation (Docstrings) :

Documentez vos classes, méthodes et fonctions à l'aide de docstrings. Python a


des conventions spécifiques pour les docstrings (PEP 257). [105]

Les docstrings doivent expliquer le but de l'entité, ses arguments, ce qu'elle


retourne, et les exceptions qu'elle peut lever.

```python class Calculatrice: """Représente une calculatrice simple.


Permet d'effectuer des opérations arithmétiques de base.
"""
def additionner(self, a, b):
"""Additionne deux nombres et retourne le résultat.

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

```

Éviter les Dépendances Circulaires :

Les dépendances circulaires (où module A importe module B, et module B importe


module A) peuvent rendre le code difficile à comprendre, à tester et à maintenir.
[106]

La Clean Architecture aide à prévenir cela en imposant la règle de dépendance


unidirectionnelle (vers l'intérieur).

Si une dépendance circulaire apparaît, cela peut indiquer une violation du SRP ou
un besoin de refactoriser en introduisant une abstraction.

Utiliser les Exceptions pour la Gestion des Erreurs :

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.

```python class ProduitService: def get_produit_par_id(self, id_produit): # Supposons


que [Link].get_by_id retourne None si non trouvé produit =
[Link].get_by_id(id_produit) if produit is None: raise
ProduitNonTrouveError(f"Produit avec ID {id_produit} non trouvé.") return produit

class ProduitNonTrouveError(Exception): pass ```

Principe KISS (Keep It Simple, Stupid) :

Privilégiez la simplicité et la clarté. Un code simple est plus facile à comprendre, à


déboguer et à maintenir. [108]

Évitez la complexité inutile et les abstractions excessives si elles n'apportent pas de


valeur ajoutée significative.

Principe DRY (Don't Repeat Yourself) :


Évitez la duplication de code. Si vous vous retrouvez à écrire le même code
plusieurs fois, c'est un signe qu'il devrait être factorisé dans une fonction, une
méthode ou une classe réutilisable. [109]

L'héritage et la composition sont des outils clés pour appliquer le DRY.

8.2. L'Importance des Tests dans une Architecture Propre

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

Exemple de test unitaire pour un cas


d'utilisation (mocking du repository)
import unittest from [Link] import Mock from application.use_cases import
ProductUseCases from [Link] import ProductCreateDTO from [Link]
import Product

class TestProductUseCases([Link]): def setUp(self): self.mock_repo = Mock() #


Crée un mock pour IProductRepository self.use_case =
ProductUseCases(product_repo=self.mock_repo)
def test_create_product(self):
dto = ProductCreateDTO(name="Test Prod", description="Desc", price=10.0,
stock=5)
# Configure le mock pour qu'il ne fasse rien quand add est appelé
self.mock_repo.add.return_value = None

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 :

Vérifient l'interaction entre plusieurs composants ou couches (par exemple, un cas


d'utilisation avec une implémentation réelle du repository et une base de
données). [113]

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

Exemple de test d'intégration (nécessite


une base de données réelle ou en
mémoire)

Ceci est un exemple conceptuel,


l'implémentation réelle dépendrait du
framework de test et de l'ORM
import unittest from application.use_cases import ProductUseCases from
[Link] import ProductCreateDTO from
[Link].in_memory_product_repository import
InMemoryProductRepository

class TestProductIntegration([Link]): def setUp(self): # Utilise une


implémentation réelle du repository [Link] = InMemoryProductRepository()
self.use_case = ProductUseCases(product_repo=[Link])

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.

Facilite le Refactoring : Vous pouvez modifier le code en toute confiance, sachant


que les tests vous alerteront si vous cassez quelque chose.

Documentation Vivante : Les tests servent de documentation sur le


comportement attendu du code. [114]

8.3. Conclusion : Les Bénéfices d'une Architecture Solide

Ce cours vous a guidé à travers les concepts fondamentaux de la Programmation Orientée


Objet en Python, en allant au-delà des bases pour explorer des approches architecturales
avancées comme la Clean Architecture et les principes SOLID. En adoptant ces pratiques, vous
pouvez transformer la manière dont vous concevez et développez des applications Python.

Les principaux bénéfices de cette approche sont :

Maintenabilité Accrue : La séparation claire des préoccupations et le faible couplage


entre les modules rendent le code plus facile à comprendre, à modifier et à déboguer.
Les changements dans une partie du système ont un impact minimal sur les autres. [115]

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)

Vous aimerez peut-être aussi