Cette partie est largement inspirée des concepts et exemples présentés dans le livre :
Clean Architecture, A Craftsman’s Guide to Software Structure and Design. Robert C. Martin 2017.
Architecture applicative : une définition
L’architecture applicative dĂ©signe l’organisation structurelle d’une application logicielle, englobant ses composants, leurs interactions et les principes sous-jacents Ă leur conception. Elle Ă©tablit une feuille de route qui guide le dĂ©veloppement, le dĂ©ploiement, et l’Ă©volution de l’application en fonction des besoins techniques, fonctionnels et stratĂ©giques.
En d’autres termes c’est une organisation proposĂ©e des sources de nos programmes qui permet ensuite une bonne Ă©volution de celui ci.
Mais pour quel but?
The goal of software architecture is to minimize the human resources required to build and maintain the required system.
Priorisation, matrice d’Eisenhower
La rĂ©alisation de travaux et de programmes est consommatrice de temps et de ressources humaines, il est donc important d’identifier les tĂąches a faire et les ordonner selon leur importance et leur criticitĂ©.
Urgence et Importance | Action a faire |
---|---|
Urgent et Important | Ă faire rapidement. |
Urgent et non important | Déléguer si possible : Ces tùches sont urgentes mais ont un impact moindre. Exemple : répondre à certains e-mails ou résoudre des problÚmes mineurs. |
Non Urgent mais important | Ă planifier : Ces tĂąches doivent ĂȘtre intĂ©grĂ©es dans votre emploi du temps pour garantir leur rĂ©alisation sans pression. |
Non Urgent et non important | Ăliminer ou minimiser : Ces activitĂ©s nâapportent pas de valeur ajoutĂ©e et peuvent ĂȘtre une perte de temps. Exemple : les distractions inutiles ou les tĂąches peu significatives. |
Pour utiliser cette matrices:
- Listez toutes vos tĂąches.
- Classez-les dans lâun des quatre quadrants.
Vous pouvez ensuite affiner la difficultĂ© des diffĂ©rentes tĂąches a effectuer pour vous permettre d’identifier des prioritĂ©s a la rĂ©alisation de ces tĂąches. Puis enfin organiser vos Ă©quipes pour rĂ©aliser ces tĂąches dans des dĂ©lais raisonnables.
Dans l’Ă©volution des mĂ©thodes de travail, l’Ă©mergence des mĂ©thodes agiles propose une organisation du travail autour de cette planification au plus proche du besoin.
Pour aller plus loin : https://fr.wikipedia.org/wiki/M%C3%A9thode_agile
Modules et fonctions
Un module en Python est un fichier contenant du code Python. Il peut inclure des fonctions, des classes, des variables, et mĂȘme des instructions exĂ©cutables. Un module permet de regrouper des fonctionnalitĂ©s spĂ©cifiques dans un fichier distinct afin de rendre le code plus rĂ©utilisable, organisĂ© et lisible.
En Python, les modules sont simplement des fichiers avec l’extension .py. Vous pouvez crĂ©er votre propre module ou utiliser des modules standard fournis avec Python. Pour utiliser un module, vous devez l’importer dans votre programme avec la commande import.
Voir aussi: https://peps.python.org/pep-0002/
Les fonctions permettent de regrouper des instructions pour accomplir une tĂąche spĂ©cifique. Elles favorisent la rĂ©utilisabilitĂ© et l’organisation du code.
def battre_oeufs():
"""Simule le fait de battre les Ćufs."""
print("Battre les Ćufs jusqu'Ă ce qu'ils soient bien mousseux.")
def faire_fondre_beurre():
"""Simule le fait de faire fondre le beurre."""
print("Faire fondre le beurre dans une casserole.")
def faire_fondre_chocolat():
"""Simule le fait de faire fondre le chocolat."""
print("Faire fondre le chocolat au bain-marie.")
def preparer_mousse_au_chocolat():
"""Prépare la mousse au chocolat en appelant les autres fonctions dans l'ordre."""
print("Préparation de la mousse au chocolat :")
battre_oeufs()
faire_fondre_beurre()
faire_fondre_chocolat()
print("Mélanger tous les ingrédients pour former une mousse délicieuse!")
# Appel de la fonction principale pour préparer la mousse au chocolat
preparer_mousse_au_chocolat()
Bilan : Les modules et fonctions permettent ainsi de créer des programmes bien structurés, organisés autour de blocs logiques qui favorisent la réutilisation et la compréhension du code.
Programmation orientĂ©e objet : principes de l’objet
La programmation orientée objet (POO) repose sur plusieurs principes fondamentaux, parmi lesquels : polymorphisme, encapsulation et héritage. Ces concepts permettent de structurer le code de maniÚre modulaire, réutilisable et extensible.
1. Polymorphisme
Le polymorphisme permet Ă une mĂȘme mĂ©thode ou fonction d’avoir des comportements diffĂ©rents selon le contexte ou le type de donnĂ©es.
Exemple concret : SystĂšme de paiement
Imaginons un systÚme de paiement qui accepte plusieurs méthodes de paiement (carte bancaire, PayPal, Bitcoin). Chaque méthode a sa propre logique, mais toutes doivent implémenter une méthode pay()
.
class PaymentMethod:
def pay(self, amount):
pass
class CreditCard(PaymentMethod):
def pay(self, amount):
print(f"Payment of {amount} made using Credit Card.")
class PayPal(PaymentMethod):
def pay(self, amount):
print(f"Payment of {amount} made using PayPal.")
class Bitcoin(PaymentMethod):
def pay(self, amount):
print(f"Payment of {amount} made using Bitcoin.")
# Polymorphisme : utilisation de différentes méthodes de paiement
def process_payment(payment_method, amount):
payment_method.pay(amount)
# Utilisation
credit_card = CreditCard()
paypal = PayPal()
bitcoin = Bitcoin()
process_payment(credit_card, 100) # Payment of 100 made using Credit Card.
process_payment(paypal, 200) # Payment of 200 made using PayPal.
process_payment(bitcoin, 300) # Payment of 300 made using Bitcoin.
2. Encapsulation
L’encapsulation consiste Ă restreindre l’accĂšs direct aux donnĂ©es internes d’une classe et Ă les protĂ©ger contre les modifications non contrĂŽlĂ©es. Cela est rĂ©alisĂ© en dĂ©finissant des attributs privĂ©s ou protĂ©gĂ©s et en utilisant des mĂ©thodes d’accĂšs (getters et setters).
Exemple ici avec la gestion des paiements dans un SI bancaire :
class BankAccount:
def __init__(self, owner, balance, credit_limit):
self.owner = owner
self.__balance = balance # Attribut privé
self.__credit_limit = credit_limit # Attribut privé
# Getter pour accéder au solde
def get_balance(self):
return self.__balance
# Méthode pour savoir si l'utilisateur peut payer une certaine somme
def can_pay(self, amount):
return amount <= self.__balance + self.__credit_limit
# DépÎt d'argent
def deposit(self, amount):
if amount > 0:
self.__balance += amount
else:
raise ValueError("Le montant doit ĂȘtre positif.")
# Retrait d'argent (avec vérification de la possibilité de paiement)
def withdraw(self, amount):
if amount <= self.__balance:
self.__balance -= amount
elif amount <= self.__balance + self.__credit_limit:
# Utilisation du crédit autorisé si le solde est insuffisant
self.__credit_limit -= (amount - self.__balance)
self.__balance = 0
else:
raise ValueError("Solde insuffisant et limite de crédit atteinte.")
# Utilisation
account = BankAccount("Alice", 1000, 500) # Alice a un solde de 1000 et une limite de crédit de 500
# Vérifier si Alice peut payer
print(account.can_pay(1200)) # True, car 1000 (solde) + 500 (crédit) = 1500, suffisant pour 1200
print(account.can_pay(1700)) # False, car 1000 + 500 = 1500, insuffisant pour 1700
# Retrait
account.withdraw(1200) # Utilisation du crédit de 200, il reste 100 dans le solde
print(account.can_pay(100)) # True, car 0 (solde) + 500 (crédit) suffisant pour 100
On remarque qu’on a pas besoin de connaĂźtre le montant du compte en banque d’alice pour rĂ©aliser l’opĂ©ration, mais que cela est fait de maniĂšre intrinsĂšque a l’opĂ©ration de consommation et de “can_pay”.
3. Héritage
L’hĂ©ritage permet de crĂ©er de nouvelles classes (sous-classes) en rĂ©utilisant le code d’une classe existante (classe parente). Cela favorise la rĂ©utilisabilitĂ© et l’organisation du code. En Python, une sous-classe hĂ©rite de toutes les mĂ©thodes et attributs de la classe parente, mais peut aussi redĂ©finir des comportements spĂ©cifiques.
Exemple avec une classe d’un site de reservation proposant plusieurs services
# Classe parente : Reservation
class Reservation:
def __init__(self, name, date, price):
self.name = name # Nom de la personne qui réserve
self.date = date # Date de la réservation
self.price = price # Prix de la réservation
def display_info(self):
print(f"RĂ©servation pour {self.name} Ă la date du {self.date}. Prix: {self.price}âŹ")
def confirm(self):
print(f"La réservation pour {self.name} est confirmée.")
# Classe enfant : HotelReservation (hérite de Reservation)
class HotelReservation(Reservation):
def __init__(self, name, date, price, num_nights):
super().__init__(name, date, price) # Appel au constructeur de la classe parente
self.num_nights = num_nights # Nombre de nuits réservées
def display_info(self):
super().display_info() # Appel de la méthode de la classe parente
print(f"Nombre de nuits: {self.num_nights}")
def confirm(self):
super().confirm() # Appel de la méthode de la classe parente
print(f"Réservation de l'hÎtel pour {self.num_nights} nuit(s) confirmée.")
# Classe enfant : FlightReservation (hérite de Reservation)
class FlightReservation(Reservation):
def __init__(self, name, date, price, flight_number):
super().__init__(name, date, price) # Appel au constructeur de la classe parente
self.flight_number = flight_number # Numéro de vol
def display_info(self):
super().display_info() # Appel de la méthode de la classe parente
print(f"Numéro de vol: {self.flight_number}")
def confirm(self):
super().confirm() # Appel de la méthode de la classe parente
print(f"Réservation du vol {self.flight_number} confirmée.")
# Utilisation des classes
# Création d'une réservation d'hÎtel
hotel_reservation = HotelReservation("Alice", "2023-12-25", 200, 3)
hotel_reservation.display_info()
hotel_reservation.confirm()
# Création d'une réservation de vol
flight_reservation = FlightReservation("Bob", "2023-12-25", 150, "AF1234")
flight_reservation.display_info()
flight_reservation.confirm()
Programmation orientée objet : Principes SOLID
1. SRP : Principe de Responsabilité Unique (Single Responsibility Principle)
Un module logiciel (classe, fonction, etc.) ne doit avoir qu’une seule raison de changer.
# Mauvaise pratique : une seule classe gÚre plusieurs responsabilités.
class Order:
def calculate_total(self):
pass # Calcul du total
def print_order(self):
pass # Imprime la commande
def save_to_db(self):
pass # Sauvegarde dans la base de données
# Bonne pratique : chaque classe gÚre une seule responsabilité.
class Order:
def calculate_total(self):
pass
class OrderPrinter:
def print_order(self, order):
pass
class OrderRepository:
def save_to_db(self, order):
pass
Cela implique donc qu’il faut sĂ©parer fonctionnellement les implĂ©mentations entre des couches bien distinctes => Vues, Business_object, …
2. OCP : Principe Ouvert-Fermé (Open-Closed Principle)
Les modules logiciels doivent ĂȘtre ouverts Ă lâextension, mais fermĂ©s Ă la modification.
# Mauvaise pratique : modification du code existant pour ajouter un comportement.
class Discount:
def apply_discount(self, price, discount_type):
if discount_type == "percentage":
return price * 0.9
elif discount_type == "fixed":
return price - 10
# Bonne pratique : extension via des classes dérivées.
from abc import ABC, abstractmethod
class Discount(ABC):
@abstractmethod
def apply_discount(self, price):
pass
class PercentageDiscount(Discount):
def apply_discount(self, price):
return price * 0.9
class FixedDiscount(Discount):
def apply_discount(self, price):
return price - 10
Cela vous permet de définir autant de versions différentes par extensions plutÎt que de créer des chaines de if.
3. LSP : Principe de Substitution de Liskov (Liskov Substitution Principle)
Les sous-types doivent ĂȘtre substituables Ă leurs types parents.
=> Les enfants doivent respecter le contrat d’interface des parents.
# Mauvaise pratique : la classe dérivée casse le contrat de la classe parent.
class Bird:
def fly(self):
pass
class Penguin(Bird):
def fly(self):
raise Exception("Les pingouins ne volent pas!")
# Bonne pratique : refactorisation pour respecter le contrat.
from abc import ABC, abstractmethod
class Bird(ABC):
@abstractmethod
def move(self):
pass
class FlyingBird(Bird):
def move(self):
print("Je vole!")
class NonFlyingBird(Bird):
def move(self):
print("Je marche!")
4. ISP : Principe de Ségrégation des Interfaces (Interface Segregation Principle)
Un client ne doit pas ĂȘtre forcĂ© de dĂ©pendre d’interfaces qu’il n’utilise pas.
- Ce principe prĂŽne la conception dâinterfaces spĂ©cifiques Ă un usage prĂ©cis, Ă©vitant les dĂ©pendances inutiles.
Il faut donc sĂ©parer lorsqu’il y a des dĂ©pendances non pertinentes pour dĂ©finir un objet en plusieurs sous dĂ©pendances.
# Mauvaise pratique : une interface trop large.
class Animal:
def eat(self):
pass
def fly(self):
pass
def swim(self):
pass
class Dog(Animal):
def eat(self):
pass
def fly(self): # Non pertinent pour un chien.
raise NotImplementedError()
def swim(self):
pass
# Bonne pratique : des interfaces spécifiques.
from abc import ABC, abstractmethod
class Eater(ABC):
@abstractmethod
def eat(self):
pass
class Swimmer(ABC):
@abstractmethod
def swim(self):
pass
class Dog(Eater, Swimmer):
def eat(self):
print("Je mange!")
def swim(self):
print("Je nage!")
5. DIP : Principe d’Inversion des DĂ©pendances (Dependency Inversion Principle)
Les modules de haut niveau ne doivent pas dépendre des modules de bas niveau.
- Les dĂ©tails doivent dĂ©pendre des abstractions, et non lâinverse. Cela permet une sĂ©paration claire entre la logique mĂ©tier et les dĂ©tails dâimplĂ©mentation.
# Mauvaise pratique : dépendance directe sur une implémentation.
class Database:
def connect(self):
print("Connexion à la base de données...")
class UserRepository:
def __init__(self):
self.db = Database()
def get_user(self, user_id):
self.db.connect()
print(f"Récupération de l'utilisateur {user_id}")
# Bonne pratique : dépendance sur une abstraction.
from abc import ABC, abstractmethod
class Database(ABC):
@abstractmethod
def connect(self):
pass
class MySQLDatabase(Database):
def connect(self):
print("Connexion Ă MySQL...")
class UserRepository:
def __init__(self, db: Database):
self.db = db
def get_user(self, user_id):
self.db.connect()
print(f"Récupération de l'utilisateur {user_id}")
# Utilisation
db = MySQLDatabase()
repo = UserRepository(db)
repo.get_user(1)
Il faut ici noter qu’on injecte des interfaces plutĂŽt que des implĂ©mentations et donc cela permet de changer l’implĂ©mentation sans avoir a toucher a la classe fille
Importance des Principes SOLID
Ces principes permettent :
- Une meilleure maintenabilité : le code est plus facile à comprendre, tester et modifier.
- Une meilleure Ă©volutivitĂ© : les modifications ou extensions du logiciel nâaffectent pas ses parties stables.
Il faudra donc isoler les couches applicatives indĂ©pendantes, en ne faisant pas de calculs dans les parties d’affichage, en ne partageant pas d’accĂšs aux bases de donnĂ©es avec les calculs mais bien en isolant les diffĂ©rents modules applicatifs dans le code.
Architecture d’une application multi couches
L’architecture multi-couches permet de sĂ©parer les diffĂ©rentes responsabilitĂ©s d’une application en plusieurs couches. On parle aussi d’architecture n tiers. Dans le modĂšle classique client-serveur, nous avons trois couches principales :
- Couche Client (ou prĂ©sentation) : Interagit avec l’utilisateur, collecte ses donnĂ©es, et affiche les rĂ©sultats.
- Couche Serveur (ou logique métier) : Traite les données envoyées par le client, applique la logique métier, et communique avec la base de données.
- Couche Base de DonnĂ©es (ou stockage des donnĂ©es) : Stocke et gĂšre les donnĂ©es de l’application, et rĂ©pond aux requĂȘtes envoyĂ©es par le serveur.
Chaque couche est responsable d’une partie spĂ©cifique de l’application, ce qui facilite la maintenabilitĂ©, la scalabilitĂ©, et la flexibilitĂ© du systĂšme.
Lors de la rĂ©alisation de vos projets, il vous sera demandĂ© d’effectuer un diagramme uml de sĂ©quence de vos systĂšmes pour des fonctionnalitĂ©s. Cela permettra de mettre en exergue les interactions au sein de votre systĂšme.
Modules et couches : Organisation du code
Cette partie vous prĂ©sente des propositions d’organisation du code en modules cohĂ©rents. Il faudra que votre code puisse ĂȘtre accessible par un nĂ©ophyte n’ayant pas travaillĂ© sur le projet.
Python - fastapi/flask
- Un dossier pour l’application :
app
- Un sous dossier de app pour chaque couche applicative :
router/controller
dao
business_object
…
/app
/app/main.py #(ou autre mais point d'entrée)
/app/requirements.txt #(ou poetry)
/app/README.md #(documentation de l'app avec comment la lancer)
/app/business_object/ #(modÚle de données)
/app/dao/ #(accÚs au données)
/app/controller/ #(ou controller c'est selon)
/app/service/ #(classes et méthodes de service)
Python - django
/app
/app/manage.py
/app/README.md
/app/config/ # application principale (exemple mysite)
/app/config/settings.py
/app/config/urls.py
/app/config/...
/app/requirements.txt
/app/.env/
/app/apps/monappli/ # django gĂšre sur un mĂȘme serveur plusieurs applis
/app/apps/monappli/models.py # ModÚles de données (ORM)
/app/apps/monappli/dao.py # AccÚs aux données (Data Access Layer)
/app/apps/monappli/services.py # Logique métier
/app/apps/monappli/controllers.py # Gestion des requĂȘtes HTTP
/app/apps/monappli/urls.py # Routing de lâapplication
/app/apps/monappli/views.py # Vues Django classiques
/app/static/ # Fichiers statiques (CSS, JS, images)
/app/static/css/
/app/static/js/
/app/static/images/
IHM (voir cours 4-5)
- Suivre l’architecture proposĂ©e par vite pour les application React :
site/
site/package.json
site/package-lock.json
site/src/main.jsx # composant démarrant l'application
site/src/App.jsx # Composant principal de l'application.
site/src/assets/ # Fichiers statiques comme les styles globaux, images, polices, etc.
site/src/components/ # Composants UI réutilisables.
site/src/pages/ #Pages principales de l'application.
site/src/services/ # Fonctions liées aux appels API et logique métier. api, web, .. convient.
site/src/utils/ # Fonctions utilitaires globales.
site/public/ # Contient les fichiers accessibles directement dans le navigateur (ex: `favicon.ico`).
- Site statique simple : séparer html, css, js et documentation propre au site
/mon-site
/mon-site/index.html # (page principale)
/mon-site/about.html # (autres pages du site)
/mon-site/assets/ # (ressources)
/mon-site/assets/css/ # (fichiers CSS)
/mon-site/assets/css/styles.css
/mon-site/assets/js/ # (scripts JavaScript)
/mon-site/assets/images/ # (images et icĂŽnes)
/mon-site/README.md # (documentation du sous projet site)
/mon-site/package.json # (si utilisation de npm)