đŸ§± Architecture applicative

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 ImportanceAction a faire
Urgent et ImportantÀ faire rapidement.
Urgent et non importantDé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.

Base de DonnĂ©esServeurClientBase de DonnĂ©esServeurClientEnvoie une requĂȘte HTTP (ex: login)Valide la demande et applique la logique mĂ©tierExĂ©cute une requĂȘte SQL pour vĂ©rifier les informationsRenvoie les rĂ©sultats de la base de donnĂ©es (ex: utilisateur trouvĂ©)Envoie une rĂ©ponse HTTP avec les rĂ©sultats (ex: succĂšs ou erreur)

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)