# [CONSTRUCTION] GUIDE COMPLET DES ARCHITECTURES LOGICIELLES - ULTRA DÉTAILLÉ POUR DÉBUTANTS

## * INTRODUCTION

### Bienvenue dans le monde des architectures logicielles !

Ce guide est conçu pour les **grands débutants** en architecture logicielle. Si tu te poses des questions comme :
- "C'est quoi une architecture logicielle ?"
- "Pourquoi mon code est difficile à maintenir ?"
- "Comment organiser un gros projet ?"
- "Quelle architecture choisir pour mon application ?"

**Tu es au bon endroit ! [OBJECTIF]**

---

### [LISTE] TABLE DES MATIÈRES

Ce guide est divisé en plusieurs fichiers thématiques :

1. **architecture.txt** (CE FICHIER) - Vue d'ensemble et concepts fondamentaux
2. **architecture_monolithique.txt** - Architecture monolithique détaillée
3. **architecture_mvc.txt** - Modèle-Vue-Contrôleur (MVC)
4. **architecture_layered.txt** - Architecture en couches (N-tiers)
5. **architecture_microservices.txt** - Microservices
6. **architecture_clean.txt** - Clean Architecture
7. **architecture_hexagonale.txt** - Architecture Hexagonale (Ports & Adapters)
8. **architecture_event_driven.txt** - Architecture événementielle
9. **architecture_serverless.txt** - Serverless
10. **architecture_comparaison.txt** - Comparaison et choix d'architecture
11. **architecture_patterns.txt** - Patterns de conception (Design Patterns)
12. **architecture_pratique.txt** - Exercices et projets pratiques

---

### [OBJECTIF] Qu'est-ce qu'une architecture logicielle ?

**Définition simple :**

L'architecture logicielle, c'est **la manière dont tu organises et structures ton code** pour créer une application.

**Analogie avec une maison [ACCUEIL] :**

Imagine que tu construis une maison :

```
[CONSTRUCTION] MAISON                        [CODE] APPLICATION
─────────────────────────────────────────────────────────
[MESURE] Plans d'architecte       ->    Architecture logicielle
[BRICK] Fondations               ->    Base de données
[WINDOW] Fenêtres                 ->    Interface utilisateur (UI)
[SORTIE] Portes                   ->    API / Points d'entrée
[RAPIDE] Électricité              ->    Services / Logique métier
[POTABLE_WATER_SYMBOL] Plomberie                ->    Flux de données
[SECURITE] Murs porteurs           ->    Composants principaux
[DESIGN] Décoration               ->    Design / UX
```

**Pourquoi c'est important ?**

Sans bons plans (architecture) :
- [X] La maison risque de s'effondrer
- [X] Difficile d'ajouter une pièce plus tard
- [X] Problèmes de maintenance
- [X] Coûts élevés de réparation

Avec de bons plans :
- [OK] Maison solide et durable
- [OK] Facile d'agrandir ou de rénover
- [OK] Maintenance simple
- [OK] Économies à long terme

**Pareil pour le code ! [OBJECTIF]**

---

### [REFLEXION] Pourquoi apprendre les architectures ?

**1. Code maintenable [OUTIL]**

**Mauvais code (sans architecture) :**

```python
# Tout dans un seul fichier de 5000 lignes
def faire_tout():
    # Se connecter à la base de données
    # Vérifier l'utilisateur
    # Calculer le prix
    # Envoyer un email
    # Générer un PDF
    # Mettre à jour le stock
    # Logger l'action
    # ... [SHOCKED_FACE_WITH_EXPLODING_HEAD]
```

**Problèmes :**
- Impossible de comprendre ce qui se passe
- Une modification peut tout casser
- Difficile de tester
- Plusieurs développeurs ne peuvent pas travailler en même temps

---

**Bon code (avec architecture) :**

```python
# Séparé en modules clairs
class UserService:
    def authenticate(self, username, password):
        # Logique d'authentification
        pass

class PriceCalculator:
    def calculate(self, items, discount):
        # Calcul du prix
        pass

class EmailService:
    def send_order_confirmation(self, order):
        # Envoi d'email
        pass

class PDFGenerator:
    def generate_invoice(self, order):
        # Génération de PDF
        pass
```

**Avantages :**
- [OK] Chaque partie a un rôle clair
- [OK] Facile de modifier une partie sans toucher le reste
- [OK] Simple à tester
- [OK] Plusieurs développeurs peuvent travailler en parallèle

---

**2. Scalabilité [HAUSSE]**

**Exemple concret :**

Tu as créé une application de e-commerce.

**Sans architecture (monolithe mal conçu) :**

```
Jour 1 : 100 utilisateurs -> [OK] Ça marche
Jour 30 : 1 000 utilisateurs -> [ATTENTION] Ralentissements
Jour 90 : 10 000 utilisateurs -> [X] Crashes fréquents
Jour 180 : 100 000 utilisateurs -> [IMPACT] GAME OVER
```

**Avec architecture (bien pensée) :**

```
Jour 1 : 100 utilisateurs -> [OK] Ça marche
Jour 30 : 1 000 utilisateurs -> [OK] Ça marche
Jour 90 : 10 000 utilisateurs -> [OK] On ajoute des serveurs
Jour 180 : 100 000 utilisateurs -> [OK] On scale horizontalement
Jour 365 : 1 000 000 utilisateurs -> [OK] Architecture distribuée
```

---

**3. Travail en équipe [UTILISATEURS]**

**Scénario 1 : Sans architecture claire**

```
Développeur A : "Je vais modifier ce fichier"
Développeur B : "Moi aussi j'en ai besoin !"
Développeur A : "Attends, je n'ai pas fini"
Développeur B : "Mais j'ai une deadline !"
-> Conflits Git
-> Code qui se marche dessus
-> Bugs difficiles à tracer
```

**Scénario 2 : Avec architecture claire**

```
Développeur A : "Je m'occupe du module de paiement"
Développeur B : "Moi je fais le module de livraison"
Développeur C : "Je crée le module de notifications"
-> Travail en parallèle
-> Pas de conflits
-> Intégration facile
```

---

**4. Réutilisabilité [BLACK_UNIVERSAL_RECYCLING_SYMBOL]**

**Sans architecture :**

```python
# Code spécifique, impossible à réutiliser
def envoyer_email_commande_avec_calcul_prix_et_stock():
    # 300 lignes de code mélangé
    pass
```

Tu ne peux pas réutiliser ce code ailleurs.

**Avec architecture :**

```python
# Modules réutilisables
class EmailService:
    def send(self, to, subject, body):
        # Peut être utilisé partout
        pass

class PriceCalculator:
    def calculate(self, items):
        # Peut être utilisé dans plusieurs contextes
        pass
```

Tu peux utiliser `EmailService` pour n'importe quel email !

---

**5. Testabilité [TEST]**

**Sans architecture :**

```python
def faire_achat(user_id, product_id):
    # Connecte à la DB
    # Vérifie le stock
    # Calcule le prix
    # Envoie un email
    # Génère une facture
    # Met à jour l'inventaire
    pass

# Comment tester ça ??? [!]
# Il faut une vraie DB, un serveur email, etc.
```

**Avec architecture :**

```python
class PurchaseService:
    def __init__(self, db, email_service, inventory):
        self.db = db
        self.email_service = email_service
        self.inventory = inventory
    
    def make_purchase(self, user_id, product_id):
        # Utilise les dépendances injectées
        pass

# Tests faciles avec des mocks ! [OBJECTIF]
def test_purchase():
    mock_db = MockDatabase()
    mock_email = MockEmailService()
    mock_inventory = MockInventory()
    
    service = PurchaseService(mock_db, mock_email, mock_inventory)
    result = service.make_purchase(1, 100)
    
    assert result.success == True
```

---

### [GRAPHIQUE] Les problèmes qu'une bonne architecture résout

| Problème | Sans architecture | Avec architecture |
|----------|-------------------|-------------------|
| **Complexité** | Code spaghetti [SPAGHETTI] | Structure claire [MESURE] |
| **Maintenance** | Cauchemar [!] | Facile [SMILING_FACE_WITH_SMILING_EYES] |
| **Bugs** | Partout [BUG][BUG][BUG] | Isolés et traçables [RECHERCHE] |
| **Performances** | Lent [LENT] | Optimisable [RAPIDE] |
| **Évolutivité** | Impossible d'ajouter des features | Simple d'étendre * |
| **Tests** | Difficiles/impossibles | Simples et automatisés [OK] |
| **Collaboration** | Conflits constants [IMPACT] | Travail fluide [ACCORD] |
| **Documentation** | Inexistante ou obsolète [FICHIER][X] | Auto-documentée [DOCS][OK] |

---

### [OBJECTIF] Objectifs de ce guide

À la fin de ce guide complet, tu seras capable de :

**Niveau Débutant :**
- [OK] Comprendre ce qu'est une architecture logicielle
- [OK] Identifier les différents types d'architectures
- [OK] Organiser un petit projet de manière structurée
- [OK] Créer une application MVC simple
- [OK] Séparer ton code en couches (présentation, logique, données)

**Niveau Intermédiaire :**
- [OK] Choisir l'architecture adaptée à ton projet
- [OK] Implémenter une Clean Architecture
- [OK] Comprendre et utiliser les Design Patterns
- [OK] Créer des APIs RESTful bien structurées
- [OK] Gérer les dépendances correctement

**Niveau Avancé :**
- [OK] Concevoir une architecture microservices
- [OK] Implémenter une architecture hexagonale
- [OK] Créer des systèmes événementiels
- [OK] Optimiser pour la scalabilité
- [OK] Faire des choix architecturaux argumentés

---

### [WORLD_MAP] Parcours d'apprentissage recommandé

**Si tu débutes complètement :**

```
1. Lis CE fichier (architecture.txt) en entier
   v
2. Commence par architecture_monolithique.txt
   v
3. Puis architecture_mvc.txt
   v
4. Ensuite architecture_layered.txt
   v
5. Fais les exercices dans architecture_pratique.txt
   v
6. Continue avec les autres architectures selon tes besoins
```

**Si tu as déjà des bases :**

```
1. Survole CE fichier pour les concepts
   v
2. Va directement aux architectures qui t'intéressent
   v
3. Lis architecture_comparaison.txt pour choisir
   v
4. Approfondis avec architecture_patterns.txt
```

---

## [CLASSICAL_BUILDING] LES CONCEPTS FONDAMENTAUX

### [PACKAGE] Qu'est-ce qu'un composant ?

**Définition :**

Un composant est une **partie autonome** de ton application qui a une **responsabilité spécifique**.

**Analogie : Les organes du corps humain [ANATOMICAL_HEART]**

```
[BEATING_HEART] Cœur      -> Pompe le sang       -> Service de paiement
[LOGIQUE] Cerveau   -> Prend les décisions -> Logique métier
[NOSE] Nez       -> Détecte les odeurs  -> Service de validation
[EYE] Yeux      -> Capte les images    -> Interface utilisateur
[LUNGS] Poumons   -> Respiration         -> Service de cache
```

**Exemple concret en code :**

```python
# [X] Mauvais : Tout dans une seule fonction
def gerer_commande(user_id, items):
    # Vérifie l'utilisateur
    user = db.query("SELECT * FROM users WHERE id = ?", user_id)
    if not user:
        return "Erreur"
    
    # Calcule le prix
    total = 0
    for item in items:
        total += item.price * item.quantity
    
    # Applique la réduction
    if user.is_premium:
        total *= 0.9
    
    # Envoie un email
    send_email(user.email, "Confirmation", f"Total: {total}")
    
    # Sauvegarde
    db.query("INSERT INTO orders ...")
    
    return "OK"
```

**Problèmes :**
- Fonction trop longue
- Fait trop de choses différentes
- Difficile à tester
- Impossible de réutiliser une partie

---

```python
# [OK] Bon : Composants séparés

class UserService:
    """Composant : Gestion des utilisateurs"""
    def get_user(self, user_id):
        return self.db.query("SELECT * FROM users WHERE id = ?", user_id)
    
    def is_valid_user(self, user):
        return user is not None and user.active

class PriceCalculator:
    """Composant : Calcul des prix"""
    def calculate_total(self, items):
        return sum(item.price * item.quantity for item in items)
    
    def apply_discount(self, total, user):
        if user.is_premium:
            return total * 0.9
        return total

class EmailService:
    """Composant : Envoi d'emails"""
    def send_order_confirmation(self, user, total):
        subject = "Confirmation de commande"
        body = f"Merci ! Total : {total}€"
        self.send(user.email, subject, body)

class OrderRepository:
    """Composant : Persistence des commandes"""
    def save_order(self, user_id, items, total):
        return self.db.query(
            "INSERT INTO orders (user_id, items, total) VALUES (?, ?, ?)",
            user_id, items, total
        )

# Orchestration
class OrderService:
    """Composant : Orchestration de la commande"""
    def __init__(self, user_service, price_calculator, email_service, order_repo):
        self.user_service = user_service
        self.price_calculator = price_calculator
        self.email_service = email_service
        self.order_repo = order_repo
    
    def process_order(self, user_id, items):
        # Utilise les composants
        user = self.user_service.get_user(user_id)
        
        if not self.user_service.is_valid_user(user):
            return {"error": "Utilisateur invalide"}
        
        total = self.price_calculator.calculate_total(items)
        total = self.price_calculator.apply_discount(total, user)
        
        order_id = self.order_repo.save_order(user_id, items, total)
        self.email_service.send_order_confirmation(user, total)
        
        return {"success": True, "order_id": order_id, "total": total}
```

**Avantages :**
- [OK] Chaque composant a UNE responsabilité
- [OK] Facile à tester individuellement
- [OK] Réutilisable dans d'autres contextes
- [OK] Facile à comprendre et à modifier
- [OK] Peut être développé par différentes personnes

---

### [LIEN] Qu'est-ce qu'une dépendance ?

**Définition :**

Une dépendance, c'est quand un composant **a besoin** d'un autre composant pour fonctionner.

**Analogie : Voiture [VOITURE]**

```
[VOITURE] Voiture
   ├─ Dépend de -> [BATTERY] Batterie
   ├─ Dépend de -> [FUEL_PUMP] Carburant
   ├─ Dépend de -> [WHEEL] Pneus
   └─ Dépend de -> [CLE] Clé
```

Sans ces dépendances, la voiture ne peut pas fonctionner.

**En code :**

```python
# [X] Dépendance forte (mauvais)
class OrderService:
    def process_order(self, user_id, items):
        # Crée directement les dépendances à l'intérieur
        db = MySQLDatabase()  # <- Dépendance forte !
        email = GmailService()  # <- Dépendance forte !
        
        user = db.get_user(user_id)
        # ...
        email.send(user.email, "Confirmation")
```

**Problèmes :**
- Impossible de tester sans vraie DB et vraie connexion Gmail
- Si tu veux changer de DB (PostgreSQL), il faut modifier OrderService
- OrderService est **couplé** à MySQL et Gmail

---

```python
# [OK] Injection de dépendances (bon)
class OrderService:
    def __init__(self, database, email_service):
        # Reçoit les dépendances de l'extérieur
        self.db = database  # <- Injection !
        self.email = email_service  # <- Injection !
    
    def process_order(self, user_id, items):
        user = self.db.get_user(user_id)
        # ...
        self.email.send(user.email, "Confirmation")

# Utilisation en production
production_db = MySQLDatabase()
production_email = GmailService()
order_service = OrderService(production_db, production_email)

# Utilisation en tests
test_db = MockDatabase()
test_email = MockEmailService()
order_service = OrderService(test_db, test_email)
```

**Avantages :**
- [OK] Testable avec des mocks
- [OK] Flexible (facile de changer de DB)
- [OK] OrderService ne connaît pas les détails de la DB

---

### [MODULE] Couplage vs Cohésion

**Couplage = Degré de dépendance entre les composants**

**Analogie : Personnes [UTILISATEURS]**

```
Couplage FORT (mauvais) :
[PERSONNE][PERSONNE][GIRL] Famille soudée à l'extrême
   -> Si un membre part, tout s'effondre
   -> Impossible de vivre séparément

Couplage FAIBLE (bon) :
[WAVING_HAND_SIGN] Amis respectueux
   -> Relations claires et définies
   -> Chacun peut vivre indépendamment
```

**En code :**

```python
# [X] Couplage FORT (mauvais)
class OrderService:
    def process(self):
        # Connaît tous les détails internes d'EmailService
        email = EmailService()
        email.smtp_server = "smtp.gmail.com"
        email.port = 587
        email.username = "admin@example.com"
        email.password = "secret123"
        email.send_raw_message(...)  # Utilise les détails internes
```

Si `EmailService` change son implémentation interne, `OrderService` casse.

---

```python
# [OK] Couplage FAIBLE (bon)
class OrderService:
    def __init__(self, email_service):
        self.email = email_service
    
    def process(self):
        # Ne connaît que l'interface publique
        self.email.send(to="user@example.com", subject="Order", body="...")
```

Si `EmailService` change son implémentation interne, `OrderService` continue de fonctionner.

---

**Cohésion = Degré de relation entre les éléments d'un même composant**

**Analogie : Outils [OUTIL]**

```
Cohésion FAIBLE (mauvais) :
[BOITE_A_OUTILS] Boîte à outils bordélique
   -> Marteau
   -> Recettes de cuisine
   -> Clés de voiture
   -> Chaussettes
   -> Aucun lien logique

Cohésion FORTE (bon) :
[OUTIL] Boîte à outils bien organisée
   -> Marteau
   -> Clous
   -> Tournevis
   -> Vis
   -> Tout est lié à la construction
```

**En code :**

```python
# [X] Cohésion FAIBLE (mauvais)
class UtilityService:
    def send_email(self, email):
        pass
    
    def calculate_tax(self, amount):
        pass
    
    def resize_image(self, image):
        pass
    
    def validate_credit_card(self, card_number):
        pass
    
    # Aucun lien logique entre ces méthodes !
```

---

```python
# [OK] Cohésion FORTE (bon)

# Services séparés avec cohésion forte
class EmailService:
    def send(self, to, subject, body):
        pass
    
    def send_bulk(self, recipients, subject, body):
        pass
    
    def validate_email_address(self, email):
        pass
    # Tout est lié aux emails

class TaxCalculator:
    def calculate_vat(self, amount):
        pass
    
    def calculate_income_tax(self, income):
        pass
    
    def get_tax_rate(self, country):
        pass
    # Tout est lié aux taxes

class ImageProcessor:
    def resize(self, image, width, height):
        pass
    
    def crop(self, image, x, y, width, height):
        pass
    
    def compress(self, image, quality):
        pass
    # Tout est lié au traitement d'images
```

**Règles d'or :**
- [OBJECTIF] **Couplage FAIBLE** = Composants indépendants
- [OBJECTIF] **Cohésion FORTE** = Composants focalisés sur une seule responsabilité

---

### [MESURE] Principes SOLID

**SOLID = 5 principes pour écrire du bon code orienté objet**

**Pourquoi "SOLID" ?**
C'est un acronyme inventé par Robert C. Martin (Uncle Bob).

---

#### 🅰 S - Single Responsibility Principle (SRP)

**Principe de Responsabilité Unique**

**Définition :**
Une classe doit avoir **une seule raison de changer**.

**Analogie : Emplois [PRO]**

```
[X] Mauvais : Employé multi-tâches
[PERSONNE][PRO] Jean fait :
   - Comptabilité
   - Marketing
   - Développement
   - Service client
   - Nettoyage
   -> Inefficace, stressé, erreurs

[OK] Bon : Spécialisation
[PERSONNE][PRO] Jean : Comptable
[PERSONNE][PRO] Marie : Marketeur
[PERSONNE][CODE] Paul : Développeur
[PERSONNE][PRO] Sophie : Service client
   -> Chacun est expert dans son domaine
```

**En code :**

```python
# [X] Violation du SRP
class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email
    
    def save_to_database(self):
        # Responsabilité 1 : Persistence
        db.execute("INSERT INTO users ...")
    
    def send_welcome_email(self):
        # Responsabilité 2 : Communication
        smtp.send(self.email, "Welcome!")
    
    def generate_report(self):
        # Responsabilité 3 : Reporting
        return f"Report for {self.name}"
    
    def validate_email(self):
        # Responsabilité 4 : Validation
        return "@" in self.email
```

**Problèmes :**
- Si le format de la DB change -> Il faut modifier `User`
- Si le service email change -> Il faut modifier `User`
- Si le format de rapport change -> Il faut modifier `User`
- **User a 4 raisons de changer !**

---

```python
# [OK] Respect du SRP

class User:
    """Responsabilité : Représenter un utilisateur"""
    def __init__(self, name, email):
        self.name = name
        self.email = email

class UserRepository:
    """Responsabilité : Sauvegarder les utilisateurs"""
    def save(self, user):
        db.execute("INSERT INTO users (name, email) VALUES (?, ?)",
                   user.name, user.email)
    
    def find_by_email(self, email):
        return db.query("SELECT * FROM users WHERE email = ?", email)

class EmailService:
    """Responsabilité : Envoyer des emails"""
    def send_welcome_email(self, user):
        subject = "Bienvenue !"
        body = f"Bonjour {user.name}, bienvenue sur notre plateforme."
        smtp.send(user.email, subject, body)

class ReportGenerator:
    """Responsabilité : Générer des rapports"""
    def generate_user_report(self, user):
        return f"""
        Rapport Utilisateur
        -------------------
        Nom : {user.name}
        Email : {user.email}
        """

class EmailValidator:
    """Responsabilité : Valider les emails"""
    def is_valid(self, email):
        return "@" in email and "." in email
```

**Avantages :**
- [OK] Chaque classe a UNE seule raison de changer
- [OK] Facile à tester
- [OK] Réutilisable
- [OK] Facile à comprendre

---

#### 🅾 O - Open/Closed Principle (OCP)

**Principe Ouvert/Fermé**

**Définition :**
Une classe doit être **ouverte à l'extension** mais **fermée à la modification**.

**Traduction :**
Tu peux ajouter de nouvelles fonctionnalités **sans modifier** le code existant.

**Analogie : Prise électrique [PLUGIN]**

```
[OK] Bon design : Prise universelle
[PLUGIN] Prise murale (fermée à modification)
   ├─ Brancher téléphone [OK]
   ├─ Brancher ordinateur [OK]
   ├─ Brancher lampe [OK]
   └─ Brancher nouvel appareil [OK]
   -> Pas besoin de changer la prise !

[X] Mauvais design : Prise spécifique
[PLUGIN] Prise pour téléphone uniquement
   -> Pour brancher un PC, il faut CHANGER la prise
```

**En code :**

```python
# [X] Violation de OCP
class PaymentProcessor:
    def process_payment(self, payment_type, amount):
        if payment_type == "credit_card":
            # Traitement carte de crédit
            print(f"Paiement par carte : {amount}€")
        elif payment_type == "paypal":
            # Traitement PayPal
            print(f"Paiement PayPal : {amount}€")
        elif payment_type == "bitcoin":
            # Traitement Bitcoin
            print(f"Paiement Bitcoin : {amount}€")
        # Si on veut ajouter un nouveau moyen de paiement,
        # il faut MODIFIER cette classe !
```

**Problème :**
Pour ajouter Apple Pay, il faut modifier `PaymentProcessor` -> Risque de casser le code existant.

---

```python
# [OK] Respect de OCP

from abc import ABC, abstractmethod

class PaymentMethod(ABC):
    """Interface (contrat) pour les moyens de paiement"""
    @abstractmethod
    def process(self, amount):
        pass

class CreditCardPayment(PaymentMethod):
    def process(self, amount):
        print(f"Paiement par carte : {amount}€")
        # Logique spécifique carte de crédit

class PayPalPayment(PaymentMethod):
    def process(self, amount):
        print(f"Paiement PayPal : {amount}€")
        # Logique spécifique PayPal

class BitcoinPayment(PaymentMethod):
    def process(self, amount):
        print(f"Paiement Bitcoin : {amount}€")
        # Logique spécifique Bitcoin

class PaymentProcessor:
    """Fermé à modification, ouvert à extension"""
    def process_payment(self, payment_method: PaymentMethod, amount):
        # Ne connaît pas les détails de chaque moyen de paiement
        payment_method.process(amount)

# Utilisation
processor = PaymentProcessor()
processor.process_payment(CreditCardPayment(), 100)
processor.process_payment(PayPalPayment(), 50)
processor.process_payment(BitcoinPayment(), 0.005)

# Ajouter un nouveau moyen de paiement (extension)
class ApplePayPayment(PaymentMethod):
    def process(self, amount):
        print(f"Paiement Apple Pay : {amount}€")

# Pas besoin de modifier PaymentProcessor !
processor.process_payment(ApplePayPayment(), 75)
```

**Avantages :**
- [OK] Ajouter de nouveaux moyens de paiement sans toucher au code existant
- [OK] Risque zéro de casser le code qui marche
- [OK] Respect du principe de substitution

---

#### 🅻 L - Liskov Substitution Principle (LSP)

**Principe de Substitution de Liskov**

**Définition :**
Les objets d'une classe dérivée doivent pouvoir **remplacer** les objets de la classe de base **sans altérer le comportement du programme**.

**Traduction simple :**
Si tu as une classe parente et des classes enfants, tu dois pouvoir utiliser n'importe quel enfant **à la place** du parent sans problème.

**Analogie : Véhicules [VOITURE]**

```
Classe parente : Véhicule

[OK] Bon :
Voiture hérite de Véhicule
   - Peut démarrer [OK]
   - Peut accélérer [OK]
   - Peut freiner [OK]
Tout ce qu'un Véhicule fait, une Voiture le fait.

[X] Mauvais :
Vélo hérite de Véhicule
   - Peut démarrer... mais comment ? [X] (pas de moteur)
   - Peut accélérer [OK]
   - Peut freiner [OK]
Le Vélo ne peut pas faire tout ce qu'un Véhicule fait.
```

**En code :**

```python
# [X] Violation de LSP

class Bird:
    def fly(self):
        print("L'oiseau vole")

class Sparrow(Bird):
    def fly(self):
        print("Le moineau vole")  # [OK] OK

class Penguin(Bird):
    def fly(self):
        # [X] PROBLÈME : Les pingouins ne volent pas !
        raise Exception("Les pingouins ne peuvent pas voler")

# Utilisation
def make_bird_fly(bird: Bird):
    bird.fly()

make_bird_fly(Sparrow())  # [OK] Fonctionne
make_bird_fly(Penguin())  # [X] CRASH !
```

**Problème :**
`Penguin` ne peut pas remplacer `Bird` sans causer d'erreur.

---

```python
# [OK] Respect de LSP

class Bird:
    def move(self):
        pass  # Méthode abstraite

class FlyingBird(Bird):
    def move(self):
        print("Vole dans les airs")
    
    def fly(self):
        print("Utilise ses ailes pour voler")

class Sparrow(FlyingBird):
    def move(self):
        print("Le moineau vole")

class Penguin(Bird):
    def move(self):
        print("Le pingouin nage")  # [OK] Pas de vol, juste mouvement
    
    def swim(self):
        print("Nage dans l'eau")

# Utilisation
def make_bird_move(bird: Bird):
    bird.move()

make_bird_move(Sparrow())  # [OK] Le moineau vole
make_bird_move(Penguin())  # [OK] Le pingouin nage
```

**Avantages :**
- [OK] Tous les oiseaux peuvent "se déplacer"
- [OK] Pas d'exception inattendue
- [OK] Hiérarchie logique

---

#### 🅸 I - Interface Segregation Principle (ISP)

**Principe de Ségrégation des Interfaces**

**Définition :**
Les clients ne doivent pas être forcés de dépendre d'interfaces qu'ils n'utilisent pas.

**Traduction simple :**
Mieux vaut avoir **plusieurs petites interfaces spécifiques** qu'une **grosse interface générique**.

**Analogie : Télécommandes [TELEVISION]**

```
[X] Mauvais : Télécommande universelle complexe
[CONTROL_KNOBS] Télécommande avec 100 boutons
   -> Pour regarder la TV, tu n'utilises que 10 boutons
   -> Les 90 autres sont inutiles et déroutants

[OK] Bon : Télécommandes spécialisées
[TELEVISION] Télécommande TV : 10 boutons essentiels
[VIDEO_GAME] Manette de jeu : Boutons pour jouer
[SON] Télécommande audio : Contrôles de son
   -> Chaque appareil a ce dont il a besoin
```

**En code :**

```python
# [X] Violation de ISP

class Worker:
    def work(self):
        pass
    
    def eat(self):
        pass
    
    def sleep(self):
        pass

class HumanWorker(Worker):
    def work(self):
        print("L'humain travaille")
    
    def eat(self):
        print("L'humain mange")
    
    def sleep(self):
        print("L'humain dort")

class RobotWorker(Worker):
    def work(self):
        print("Le robot travaille")
    
    def eat(self):
        # [X] PROBLÈME : Les robots ne mangent pas !
        raise NotImplementedError("Les robots ne mangent pas")
    
    def sleep(self):
        # [X] PROBLÈME : Les robots ne dorment pas !
        raise NotImplementedError("Les robots ne dorment pas")
```

**Problème :**
`RobotWorker` est forcé d'implémenter `eat()` et `sleep()` alors qu'il n'en a pas besoin.

---

```python
# [OK] Respect de ISP

class Workable:
    def work(self):
        pass

class Eatable:
    def eat(self):
        pass

class Sleepable:
    def sleep(self):
        pass

class HumanWorker(Workable, Eatable, Sleepable):
    def work(self):
        print("L'humain travaille")
    
    def eat(self):
        print("L'humain mange")
    
    def sleep(self):
        print("L'humain dort")

class RobotWorker(Workable):
    def work(self):
        print("Le robot travaille")
    
    # Pas besoin d'implémenter eat() ou sleep() !

# Utilisation
def make_work(worker: Workable):
    worker.work()

def make_eat(eater: Eatable):
    eater.eat()

human = HumanWorker()
robot = RobotWorker()

make_work(human)  # [OK]
make_work(robot)  # [OK]
make_eat(human)   # [OK]
# make_eat(robot) <- Ne compile même pas, car Robot n'est pas Eatable
```

**Avantages :**
- [OK] Chaque classe implémente uniquement ce dont elle a besoin
- [OK] Interfaces petites et focalisées
- [OK] Pas de méthodes inutiles

---

#### 🅳 D - Dependency Inversion Principle (DIP)

**Principe d'Inversion des Dépendances**

**Définition :**
Les modules de haut niveau ne doivent pas dépendre des modules de bas niveau. Les deux doivent dépendre d'**abstractions**.

**Traduction simple :**
Ne fais pas dépendre ton code directement d'implémentations concrètes. Utilise des **interfaces/abstractions** à la place.

**Analogie : Prise électrique (encore) [PLUGIN]**

```
[X] Mauvais : Dépendance directe
[IDEE] Lampe soudée directement au mur
   -> Impossible de déplacer la lampe
   -> Impossible de brancher autre chose

[OK] Bon : Abstraction (prise)
[IDEE] Lampe avec prise
[PLUGIN] Prise murale (abstraction)
   -> Peut brancher n'importe quel appareil
   -> Flexible et réutilisable
```

**En code :**

```python
# [X] Violation de DIP

class MySQLDatabase:
    def connect(self):
        print("Connexion à MySQL")
    
    def query(self, sql):
        print(f"Exécution : {sql}")
        return ["résultat"]

class UserService:
    def __init__(self):
        # Dépendance directe à MySQL !
        self.db = MySQLDatabase()
    
    def get_user(self, user_id):
        return self.db.query(f"SELECT * FROM users WHERE id = {user_id}")
```

**Problèmes :**
- `UserService` est **couplé** à MySQL
- Impossible de changer de DB sans modifier `UserService`
- Difficile de tester (besoin d'une vraie DB MySQL)

---

```python
# [OK] Respect de DIP

from abc import ABC, abstractmethod

# Abstraction (interface)
class Database(ABC):
    @abstractmethod
    def connect(self):
        pass
    
    @abstractmethod
    def query(self, sql):
        pass

# Implémentations concrètes
class MySQLDatabase(Database):
    def connect(self):
        print("Connexion à MySQL")
    
    def query(self, sql):
        print(f"MySQL : {sql}")
        return ["résultat MySQL"]

class PostgreSQLDatabase(Database):
    def connect(self):
        print("Connexion à PostgreSQL")
    
    def query(self, sql):
        print(f"PostgreSQL : {sql}")
        return ["résultat PostgreSQL"]

class MockDatabase(Database):
    def connect(self):
        print("Mock DB")
    
    def query(self, sql):
        return ["résultat de test"]

# Service de haut niveau
class UserService:
    def __init__(self, database: Database):
        # Dépend de l'abstraction, pas de l'implémentation !
        self.db = database
    
    def get_user(self, user_id):
        return self.db.query(f"SELECT * FROM users WHERE id = {user_id}")

# Utilisation en production
mysql_db = MySQLDatabase()
user_service = UserService(mysql_db)
user_service.get_user(1)

# Changement de DB (sans modifier UserService !)
postgres_db = PostgreSQLDatabase()
user_service = UserService(postgres_db)
user_service.get_user(1)

# Tests (avec mock)
mock_db = MockDatabase()
user_service = UserService(mock_db)
user_service.get_user(1)
```

**Avantages :**
- [OK] Flexibilité : Facile de changer de DB
- [OK] Testabilité : Peut utiliser des mocks
- [OK] Découplage : UserService ne connaît pas les détails de MySQL

---

### [GRAPHIQUE] Résumé SOLID

| Principe | Signification | Question clé |
|----------|---------------|--------------|
| **S**RP | Une seule responsabilité | "Cette classe fait-elle UNE seule chose ?" |
| **O**CP | Ouvert/Fermé | "Puis-je ajouter des features sans modifier le code existant ?" |
| **L**SP | Substitution de Liskov | "Puis-je remplacer le parent par l'enfant sans problème ?" |
| **I**SP | Ségrégation des interfaces | "Les interfaces sont-elles petites et focalisées ?" |
| **D**IP | Inversion des dépendances | "Est-ce que je dépends d'abstractions, pas d'implémentations ?" |

**Ces principes sont la BASE de toute bonne architecture ! [OBJECTIF]**

---

## [CONSTRUCTION] TYPES D'ARCHITECTURES - VUE D'ENSEMBLE

### [GRAPHIQUE] Tableau comparatif

Voici un aperçu rapide des principales architectures que tu vas apprendre :

| Architecture | Complexité | Scalabilité | Quand l'utiliser ? |
|--------------|------------|-------------|-------------------|
| **Monolithique** | * | ** | Petits projets, MVPs, prototypes |
| **MVC** | ** | ** | Applications web classiques |
| **En couches (N-tiers)** | ** | *** | Applications d'entreprise moyennes |
| **Clean Architecture** | **** | **** | Projets à long terme, maintenabilité |
| **Hexagonale** | **** | **** | Systèmes avec beaucoup d'intégrations |
| **Microservices** | ***** | ***** | Très grandes applications, équipes multiples |
| **Event-Driven** | **** | ***** | Systèmes temps réel, IoT, messaging |
| **Serverless** | *** | ***** | Fonctions ponctuelles, forte variabilité de charge |

---

### [OBJECTIF] Comment choisir une architecture ?

**Pose-toi ces questions :**

**1. Quelle est la taille de ton projet ?**

```
Petit projet (< 10 000 lignes de code)
   -> Monolithique ou MVC
   
Projet moyen (10 000 - 100 000 lignes)
   -> En couches ou Clean Architecture
   
Grand projet (> 100 000 lignes)
   -> Microservices ou Hexagonale
```

---

**2. Combien de développeurs travaillent dessus ?**

```
1-3 développeurs
   -> Monolithique, MVC, ou En couches
   
4-10 développeurs
   -> Clean Architecture, Hexagonale
   
10+ développeurs
   -> Microservices
```

---

**3. Quelle est la fréquence de changement ?**

```
Changements rares (projet stable)
   -> Monolithique peut suffire
   
Changements fréquents
   -> Clean Architecture (flexibilité)
   
Parties évoluent à des rythmes différents
   -> Microservices
```

---

**4. Quels sont les besoins de scalabilité ?**

```
Trafic prévisible et modéré
   -> Monolithique ou En couches
   
Trafic variable, pics imprévisibles
   -> Serverless ou Event-Driven
   
Trafic massif, croissance rapide
   -> Microservices
```

---

**5. Quelles sont les contraintes de budget/temps ?**

```
Budget serré, besoin rapide
   -> Monolithique (simple et rapide)
   
Budget moyen, projet à moyen terme
   -> MVC ou En couches
   
Budget confortable, vision long terme
   -> Clean Architecture, Microservices
```

---

### [WORLD_MAP] Évolution typique d'un projet

**Parcours classique d'une startup :**

```
Étape 1 : MVP (Minimum Viable Product)
[PACKAGE] Architecture : Monolithique
[UTILISATEURS] Équipe : 1-2 développeurs
[TEMPS] Objectif : Valider l'idée rapidement
   v
   
Étape 2 : Croissance initiale
[PACKAGE] Architecture : MVC bien structuré
[UTILISATEURS] Équipe : 3-5 développeurs
[TEMPS] Objectif : Stabiliser et ajouter des features
   v
   
Étape 3 : Scale-up
[PACKAGE] Architecture : En couches ou Clean Architecture
[UTILISATEURS] Équipe : 5-15 développeurs
[TEMPS] Objectif : Gérer la complexité croissante
   v
   
Étape 4 : Entreprise mature
[PACKAGE] Architecture : Microservices
[UTILISATEURS] Équipe : 15+ développeurs (plusieurs équipes)
[TEMPS] Objectif : Indépendance des équipes, scalabilité maximale
```

**[ATTENTION] Erreur courante :**

Ne commence PAS directement par des microservices si tu as un petit projet !

```
[X] Startup qui débute avec microservices
   -> Complexité inutile
   -> Overhead opérationnel
   -> Développement lent
   -> Coûts élevés
   -> Risque d'échec

[OK] Startup qui commence simple
   -> Validation rapide
   -> Ajustements faciles
   -> Coûts faibles
   -> Évolue vers microservices si nécessaire
```

---

## [COURS] MÉTHODOLOGIE D'APPRENTISSAGE

### [DOCS] Comment utiliser ce guide ?

**1. Approche Progressive**

Ne saute pas directement aux architectures avancées !

```
Semaine 1-2 : Concepts fondamentaux (CE FICHIER)
   -> SOLID
   -> Composants
   -> Dépendances
   v
   
Semaine 3-4 : Architecture basique
   -> Monolithique
   -> MVC
   v
   
Semaine 5-6 : Architecture intermédiaire
   -> En couches
   -> Clean Architecture
   v
   
Semaine 7-8 : Architecture avancée
   -> Hexagonale
   -> Microservices
   v
   
Semaine 9+ : Spécialisations
   -> Event-Driven
   -> Serverless
```

---

**2. Pratique, Pratique, Pratique !**

**Pour chaque architecture :**

```
1. Lis la théorie
2. Regarde les exemples
3. Code un petit projet
4. Refais le projet d'une autre manière
5. Compare les différences
```

**Projets recommandés pour pratiquer :**

**Débutant :**
- [NOTE] Todo List (MVC)
- [DOCS] Bibliothèque de livres (En couches)
- [SHOPPING_TROLLEY] Panier d'achat simple (Monolithique -> MVC)

**Intermédiaire :**
- [GUIDE] Blog avec commentaires (Clean Architecture)
- [ARGENT] Système de facturation (Hexagonale)
- [GRAPHIQUE] Dashboard d'analytics (Event-Driven)

**Avancé :**
- [SHOPPING_BAGS] E-commerce complet (Microservices)
- [SPEECH_BALLOON] Application de chat temps réel (Event-Driven)
- [CAMERA_WITH_FLASH] Traitement d'images (Serverless)

---

**3. Comprendre le POURQUOI, pas juste le COMMENT**

Pour chaque concept, demande-toi :

```
[?] POURQUOI ce choix d'architecture ?
[?] QUELS problèmes ça résout ?
[?] QUELS nouveaux problèmes ça crée ?
[?] QUAND est-ce approprié ?
[?] QUAND est-ce une mauvaise idée ?
```

---

**4. Compare et Contraste**

Après avoir appris 2-3 architectures :

```
Exercice :
1. Prends un même projet simple (ex: Todo List)
2. Implémente-le avec 3 architectures différentes
3. Compare :
   - Nombre de fichiers
   - Complexité du code
   - Facilité d'ajout de features
   - Testabilité
4. Note tes observations
```

---

### [TEST] Méthodologie de test

**Pour chaque architecture, teste :**

**1. Ajout de feature**

```
Scénario : "Je veux ajouter une fonctionnalité d'export PDF"

Questions :
- Combien de fichiers dois-je modifier ?
- Est-ce que je risque de casser du code existant ?
- Combien de temps ça va prendre ?
```

**2. Changement de technologie**

```
Scénario : "Je veux passer de MySQL à PostgreSQL"

Questions :
- Combien de code dois-je modifier ?
- Est-ce localisé ou dispersé partout ?
- Combien de tests vont casser ?
```

**3. Scalabilité**

```
Scénario : "Mon trafic a été multiplié par 10"

Questions :
- Puis-je simplement ajouter des serveurs ?
- Quels sont les goulots d'étranglement ?
- Quels composants dois-je optimiser ?
```

---

## [OBJECTIF] EXERCICES DE FIN DE SECTION

### Exercice 1 : Identifier les violations de SOLID *

**Code donné :**

```python
class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email
    
    def save(self):
        db = MySQLDatabase()
        db.connect()
        db.query(f"INSERT INTO users (name, email) VALUES ('{self.name}', '{self.email}')")
    
    def send_email(self, subject, body):
        smtp = SMTPClient()
        smtp.connect("smtp.gmail.com", 587)
        smtp.send(self.email, subject, body)
    
    def generate_pdf_report(self):
        pdf = PDFGenerator()
        return pdf.create(f"User: {self.name}\nEmail: {self.email}")
```

**Questions :**

1. Quels principes SOLID sont violés ?
2. Comment restructurer ce code ?
3. Quels sont les avantages de ta restructuration ?

**Solution dans `architecture_pratique.txt`**

---

### Exercice 2 : Couplage vs Cohésion **

**Analyse ce code :**

```python
class ShoppingCart:
    def __init__(self):
        self.items = []
    
    def add_item(self, item):
        self.items.append(item)
    
    def calculate_total(self):
        return sum(item.price for item in self.items)
    
    def send_receipt_email(self, user_email):
        # Envoi d'email
        pass
    
    def log_purchase(self):
        # Logging
        pass
    
    def update_inventory(self):
        # Mise à jour du stock
        pass
```

**Questions :**

1. Quel est le niveau de cohésion de cette classe ?
2. Y a-t-il des couplages forts ?
3. Comment améliorer la structure ?

**Solution dans `architecture_pratique.txt`**

---

### Exercice 3 : Choisir une architecture ***

**Tu dois créer une application de gestion de restaurant.**

**Fonctionnalités :**
- Gestion des menus
- Prise de commandes
- Gestion des tables
- Facturation
- Statistiques

**Contexte :**
- 2 développeurs
- Budget moyen
- Besoin de livrer en 3 mois
- Évolution future possible (multi-restaurants)

**Questions :**

1. Quelle architecture choisirais-tu ? Pourquoi ?
2. Comment organiserais-tu les composants ?
3. Comment anticiperais-tu l'évolution vers multi-restaurants ?

**Solution détaillée dans `architecture_pratique.txt`**

---

## [GUIDE] RESSOURCES ET RÉFÉRENCES

### [DOCS] Livres recommandés

**Débutant :**
- "Clean Code" - Robert C. Martin
- "Head First Design Patterns" - Eric Freeman
- "The Pragmatic Programmer" - David Thomas & Andrew Hunt

**Intermédiaire :**
- "Clean Architecture" - Robert C. Martin
- "Domain-Driven Design" - Eric Evans
- "Building Microservices" - Sam Newman

**Avancé :**
- "Patterns of Enterprise Application Architecture" - Martin Fowler
- "Implementing Domain-Driven Design" - Vaughn Vernon
- "Software Architecture: The Hard Parts" - Neal Ford

---

### [WEB] Sites web et blogs

- **Martin Fowler's Blog** : martinfowler.com
- **The Clean Code Blog** : blog.cleancoder.com
- **Microsoft Architecture Center** : docs.microsoft.com/azure/architecture
- **AWS Architecture Center** : aws.amazon.com/architecture

---

### [MOVIE_CAMERA] Chaînes YouTube

- "CodeOpinion" - Architecture patterns
- "Continuous Delivery" - Dave Farley
- "ArjanCodes" - Clean code et architecture
- "IAmTimCorey" - Architecture .NET (principes transposables)

---

### [CODE] Dépôts GitHub à étudier

Cherche des projets open-source bien architecturés :

- **Django** (Python) - MVC bien fait
- **Spring Boot** (Java) - Clean Architecture
- **Laravel** (PHP) - MVC élégant
- **NestJS** (Node.js) - Architecture modulaire

---

## [BRAVO] PROCHAINES ÉTAPES

### [RAPIDE] Par où commencer ?

**Si tu es grand débutant :**

```
1. Finis de lire CE fichier complètement
2. Assure-toi de comprendre SOLID
3. Ouvre architecture_monolithique.txt
4. Code un petit projet monolithique
5. Passe à architecture_mvc.txt
```

**Si tu as déjà des bases :**

```
1. Survole les concepts SOLID (refresh)
2. Va directement à architecture_clean.txt ou architecture_hexagonale.txt
3. Lis architecture_comparaison.txt
4. Choisis un projet personnel et applique
```

---

### [LISTE] Checklist de maîtrise des concepts fondamentaux

Avant de passer aux architectures spécifiques, vérifie que tu maîtrises :

**SOLID :**
- [ ] Je peux expliquer chaque lettre de SOLID
- [ ] Je peux identifier les violations de SOLID dans du code
- [ ] Je peux refactorer du code pour respecter SOLID

**Composants :**
- [ ] Je comprends ce qu'est un composant
- [ ] Je sais créer des composants cohésifs
- [ ] Je sais gérer les dépendances entre composants

**Couplage/Cohésion :**
- [ ] Je comprends la différence
- [ ] Je peux évaluer le couplage d'un code
- [ ] Je peux améliorer la cohésion d'une classe

**Injection de dépendances :**
- [ ] Je comprends pourquoi c'est important
- [ ] Je sais implémenter l'injection de dépendances
- [ ] Je peux créer des mocks pour les tests

**Si tu ne coches pas toutes les cases, relis les sections concernées !**

---

## [BRAVO] CONCLUSION

### [TROPHEE] Ce que tu as appris

**Concepts fondamentaux :**
- [OK] Qu'est-ce qu'une architecture logicielle
- [OK] Pourquoi c'est important
- [OK] Composants et dépendances
- [OK] Couplage et cohésion
- [OK] Principes SOLID en détail

**Vision d'ensemble :**
- [OK] Panorama des différentes architectures
- [OK] Comment choisir une architecture
- [OK] Évolution typique d'un projet

**Méthodologie :**
- [OK] Comment apprendre efficacement
- [OK] Exercices et projets pratiques
- [OK] Ressources pour approfondir

---

### [IDEE] Message final

**L'architecture logicielle n'est pas une science exacte.**

Il n'y a pas UNE bonne architecture pour tous les projets.

Le but est de :
- [OBJECTIF] Comprendre les options disponibles
- [OBJECTIF] Connaître les avantages et inconvénients de chacune
- [OBJECTIF] Faire des choix **éclairés** basés sur ton contexte

**Tu es maintenant prêt à plonger dans les architectures spécifiques ! [RAPIDE]**

---

### [EMAIL] Prochains fichiers à lire

**Ordre recommandé pour un débutant :**

```
1. architecture_monolithique.txt
   v
2. architecture_mvc.txt
   v
3. architecture_layered.txt
   v
4. architecture_clean.txt
   v
5. architecture_patterns.txt (en parallèle)
   v
6. architecture_comparaison.txt (pour consolider)
   v
7. Architectures avancées selon tes besoins
```

**Bon courage dans ton apprentissage ! ***

**N'oublie pas : La pratique est la clé. Code, casse, répare, recommence ! [FORCE]**

---

## [TEL] SUPPORT

Si tu as des questions ou besoin de clarifications :

1. Relis la section concernée
2. Fais les exercices pratiques
3. Code un petit projet
4. Consulte les ressources recommandées
5. Rejoins des communautés de développeurs (Reddit, Discord, Stack Overflow)

**Partage ton code et demande des reviews !**
C'est la meilleure façon de progresser. [OBJECTIF]

---

**[BRAVO] FÉLICITATIONS D'AVOIR TERMINÉ CE FICHIER ! [BRAVO]**

**Tu as maintenant les bases solides pour explorer les architectures spécifiques.**

**Direction : `architecture_monolithique.txt` ! [RAPIDE]**

═══════════════════════════════════════════════════════════════
FIN DU FICHIER : architecture.txt
═══════════════════════════════════════════════════════════════


# [CLASSICAL_BUILDING] ARCHITECTURE MONOLITHIQUE - GUIDE ULTRA DÉTAILLÉ

## * INTRODUCTION

### Qu'est-ce qu'une architecture monolithique ?

**Définition simple :**

Un **monolithe** est une application où **tout le code est dans un seul bloc** (un seul projet, un seul déploiement).

**Analogie : Un grand magasin [DEPARTMENT_STORE]**

```
[DEPARTMENT_STORE] GRAND MAGASIN TOUT-EN-UN
├─ [NECKTIE] Rayon vêtements
├─ [BAGUETTE_BREAD] Boulangerie
├─ [CUT_OF_MEAT] Boucherie
├─ [DOCS] Librairie
├─ [PILL] Pharmacie
└─ [CARTE] Caisses centralisées

Tout est dans LE MÊME bâtiment.
Une seule entrée, une seule sortie.
Tout est connecté.
```

**En informatique :**

```
[PACKAGE] APPLICATION MONOLITHIQUE
├─ [UTILISATEUR] Gestion des utilisateurs
├─ [SHOPPING_TROLLEY] Panier d'achat
├─ [CARTE] Paiement
├─ [PACKAGE] Gestion des stocks
├─ [EMAIL] Envoi d'emails
└─ [GRAPHIQUE] Génération de rapports

Tout dans UN SEUL programme.
Un seul serveur, un seul déploiement.
```

---

### [GRAPHIQUE] Caractéristiques principales

| Caractéristique | Description |
|-----------------|-------------|
| **Code base unique** | Tout le code dans un seul dépôt |
| **Déploiement unique** | Une seule application à déployer |
| **Base de données unique** | Généralement une seule DB |
| **Processus unique** | Tout tourne dans un seul processus |
| **Couplage fort** | Les modules sont interconnectés |

---

## [REFLEXION] POURQUOI UTILISER UN MONOLITHE ?

### [OK] Avantages

**1. Simplicité de développement [RAPIDE]**

```python
# Tout est au même endroit, facile d'accès

# monolithe/
#   ├─ users.py
#   ├─ products.py
#   ├─ orders.py
#   └─ main.py

# Dans main.py, tu peux facilement utiliser tout :
from users import create_user
from products import get_product
from orders import create_order

# Pas de communication réseau, tout est local
user = create_user("John")
product = get_product(123)
order = create_order(user, product)
```

**Avantages :**
- Pas de configuration complexe
- Pas de gestion de communication entre services
- Développement rapide

---

**2. Simplicité de déploiement [PACKAGE]**

```bash
# Déployer un monolithe = Déployer UN fichier/dossier

# Construire
python setup.py build

# Déployer (un seul fichier/package)
scp app.zip server:/var/www/
ssh server "unzip app.zip && systemctl restart app"

# C'est tout ! [OK]
```

**Versus microservices :**

```bash
# Déployer 10 microservices = 10 déploiements séparés
docker build -t user-service ...
docker build -t product-service ...
docker build -t order-service ...
# ... 7 autres services
# Configuration Kubernetes
# Configuration réseaux
# Configuration load balancers
# [!] Complexité !
```

---

**3. Performance optimale [TRANSPORT]**

**Appels de fonction locaux (monolithe) :**

```python
# Temps d'exécution : 0.001 ms (microseconde)
user = user_service.get_user(123)
```

**Appels réseau (microservices) :**

```python
# Temps d'exécution : 50-200 ms (millisecondes)
response = requests.get("http://user-service:8080/users/123")
user = response.json()

# 50,000x plus lent ! [ATTENTION]
```

**Pourquoi cette différence ?**

```
Appel local (monolithe) :
CPU -> RAM -> Fonction
[RAPIDE] Nanoseconde

Appel réseau (microservices) :
CPU -> RAM -> Réseau -> Autre serveur -> RAM -> CPU -> Fonction -> Réponse
[LENT] Millisecondes
```

---

**4. Transactions simples [CARTE]**

**Scénario : Créer une commande**

```
Étapes :
1. Vérifier le stock du produit
2. Débiter le compte de l'utilisateur
3. Créer la commande
4. Réduire le stock
5. Envoyer confirmation

Si UNE étape échoue -> Tout doit être annulé (rollback)
```

**Avec un monolithe (facile) :**

```python
def create_order(user_id, product_id):
    # Transaction atomique dans une seule DB
    with db.transaction():  # Si erreur, tout est annulé automatiquement
        product = db.get_product(product_id)
        
        if product.stock < 1:
            raise Exception("Rupture de stock")
        
        db.debit_account(user_id, product.price)
        order = db.create_order(user_id, product_id)
        db.reduce_stock(product_id, 1)
        
        return order
    # [OK] Atomique : Tout réussit ou tout échoue
```

**Avec des microservices (complexe) :**

```python
def create_order(user_id, product_id):
    # Chaque appel est dans un service différent
    
    # 1. Vérifier stock (service produit)
    product = requests.get(f"http://product-service/products/{product_id}").json()
    
    if product['stock'] < 1:
        raise Exception("Rupture de stock")
    
    # 2. Débiter compte (service paiement)
    payment = requests.post("http://payment-service/debit", 
                           json={"user_id": user_id, "amount": product['price']})
    
    if not payment.ok:
        raise Exception("Paiement échoué")
    
    # 3. Créer commande (service commande)
    order = requests.post("http://order-service/orders",
                         json={"user_id": user_id, "product_id": product_id})
    
    if not order.ok:
        # [ATTENTION] PROBLÈME : Le compte est débité mais la commande échoue !
        # Il faut faire un rollback manuel du paiement
        requests.post("http://payment-service/refund", 
                     json={"transaction_id": payment.json()['id']})
        raise Exception("Commande échouée")
    
    # 4. Réduire stock
    stock_update = requests.put(f"http://product-service/products/{product_id}/reduce")
    
    if not stock_update.ok:
        # [ATTENTION] PROBLÈME : Commande créée, paiement effectué, mais stock pas réduit !
        # Rollback de la commande ET du paiement
        # [!] Complexité exponentielle !
```

**Conclusion : Transactions = CAUCHEMAR en microservices**

---

**5. Debugging facile [BUG]**

**Avec un monolithe :**

```python
# Erreur dans les logs :
Error in order_service.create_order() at line 42
  Called from payment_service.process() at line 15
  Called from main() at line 8

# Stack trace complète [OK]
# Tu vois TOUT le parcours de l'erreur
# Tu peux mettre des breakpoints partout
# Tu débugges dans UN SEUL IDE
```

**Avec des microservices :**

```
# Erreur dans les logs :
[Service 1] : Request received
[Service 2] : Processing...
[Service 3] : ERROR!

# Mais qu'est-ce qui s'est passé dans Service 1 et 2 ? [REFLEXION]
# Il faut corréler les logs de 3 services
# Trace IDs, distributed tracing...
# Complexité ! [!]
```

---

**6. Coût réduit [ARGENT]**

**Monolithe :**

```
Serveur unique :
├─ CPU : 4 cores
├─ RAM : 8 GB
├─ Coût : 50€/mois
└─ Peut gérer 10,000 utilisateurs

Total : 50€/mois [OK]
```

**Microservices :**

```
10 services, chacun avec :
├─ CPU : 2 cores (minimum viable)
├─ RAM : 2 GB (minimum viable)
├─ Coût : 20€/mois par service
├─ Load balancer : 30€/mois
├─ API Gateway : 50€/mois
└─ Monitoring : 40€/mois

Total : 320€/mois [!]
(6.4x plus cher pour la même charge !)
```

---

**7. Idéal pour les MVP [RAPIDE]**

**MVP = Minimum Viable Product** (produit minimum viable)

**Scénario : Tu veux tester une idée d'application**

```
Objectif : Valider l'idée RAPIDEMENT
Budget : Limité
Équipe : 1-2 développeurs

Monolithe :
[OK] Développement : 2-4 semaines
[OK] Déploiement : 1 jour
[OK] Coût : 50€/mois
[OK] Modifications rapides
[OK] Pivot facile

Microservices :
[X] Développement : 8-12 semaines (configuration, infrastructure)
[X] Déploiement : 1 semaine (k8s, networking, monitoring)
[X] Coût : 300-500€/mois
[X] Modifications lentes (plusieurs services)
[X] Pivot difficile (beaucoup d'infrastructure)
```

**Exemples de succès qui ont démarré en monolithe :**
- **Amazon** : Monolithe pendant les 5 premières années
- **Netflix** : Monolithe au début
- **Facebook** : Monolithe (et largement encore aujourd'hui !)
- **Shopify** : Monolithe (avec 1.7 million de lignes de Ruby)

---

### [X] Inconvénients

**1. Scalabilité limitée [HAUSSE]**

**Problème : Tu ne peux scaler que TOUT le monolithe**

```
Scénario : Service de vidéos en ligne

Fonctionnalités :
├─ [UTILISATEUR] Authentification : 1000 requêtes/sec
├─ [RECHERCHE] Recherche : 5000 requêtes/sec
├─ [DEMARRAGE] Upload vidéo : 100 requêtes/sec
└─ [TELEVISION] Streaming : 50,000 requêtes/sec <- GOULOT !

Avec un monolithe :
Si tu veux scaler le streaming, tu dois dupliquer TOUT :
  
  Serveur 1 (monolithe complet)
  Serveur 2 (monolithe complet)
  Serveur 3 (monolithe complet)
  
  [X] Tu paies pour l'authentification, la recherche, l'upload
     sur chaque serveur, même si tu n'as besoin que du streaming !
```

**Gaspillage de ressources :**

```
3 serveurs monolithes :
├─ Authentification : 3000 req/sec (besoin : 1000) [X] Gaspillage x3
├─ Recherche : 15000 req/sec (besoin : 5000) [X] Gaspillage x3
├─ Upload : 300 req/sec (besoin : 100) [X] Gaspillage x3
└─ Streaming : 150,000 req/sec (besoin : 50,000) [OK] OK

Coût : 3 × 200€/mois = 600€/mois
```

**Avec microservices (optimal) :**

```
├─ 1 serveur Authentification : 50€/mois
├─ 1 serveur Recherche : 100€/mois
├─ 1 serveur Upload : 50€/mois
└─ 10 serveurs Streaming : 200€/mois
    (seulement le streaming est scalé !)

Coût : 400€/mois (33% moins cher)
```

---

**2. Déploiements risqués [GAME_DIE]**

**Problème : Modifier UNE ligne = Redéployer TOUT**

```python
# Tu corriges un bug dans la fonction d'email
def send_email(to, subject, body):
    # Bug fix : Encodage UTF-8
    body = body.encode('utf-8')  # <- 1 ligne modifiée
    smtp.send(to, subject, body)
```

**Conséquence :**

```
1. Tu dois tester TOUTE l'application
2. Tu dois redéployer TOUT le monolithe
3. Si erreur de déploiement -> TOUT tombe
4. Rollback = Rollback de TOUT

Risque :
[X] Modifier une petite feature peut casser une grosse feature
[X] Un bug dans le module email peut crasher le paiement
[X] Downtime de toute l'application
```

**Exemple réel :**

```
Vendredi 17h : "Je corrige juste ce petit bug d'affichage"
Déploiement : 17h30
17h31 : [IMPACT] TOUT LE SITE EST DOWN
Raison : Le "petit bug" a cassé l'initialisation de la DB
18h30 : Toujours down, clients furieux
20h00 : Rollback complet
Lundi 9h : Investigation post-mortem

Coût :
├─ Chiffre d'affaires perdu : 50,000€
├─ Heures sup' : 2,000€
├─ Réputation : -∞
└─ Confiance des développeurs : [BAISSE]
```

---

**3. Base de code volumineuse [DOCS]**

**Au fil du temps :**

```
Jour 1 :
monolithe/
├─ users.py (100 lignes)
├─ products.py (150 lignes)
└─ main.py (50 lignes)
Total : 300 lignes [OK] Gérable

Année 1 :
monolithe/
├─ users/
│   ├─ auth.py (500 lignes)
│   ├─ profile.py (400 lignes)
│   └─ permissions.py (600 lignes)
├─ products/
│   ├─ catalog.py (800 lignes)
│   ├─ search.py (1000 lignes)
│   └─ recommendations.py (700 lignes)
├─ orders/
│   ├─ cart.py (600 lignes)
│   ├─ checkout.py (900 lignes)
│   └─ tracking.py (400 lignes)
└─ ... 50 autres fichiers
Total : 50,000 lignes [ATTENTION] Commence à être lourd

Année 3 :
monolithe/
├─ 200 fichiers
├─ 300,000 lignes de code
├─ Temps de build : 15 minutes
├─ Temps de démarrage : 2 minutes
└─ IDE qui rame [!]
```

**Problèmes :**

```
[X] Difficile de s'y retrouver
[X] Modifications hasardeuses (peur de casser quelque chose)
[X] Onboarding des nouveaux dev : 2-3 mois
[X] Tests qui prennent 1 heure à tourner
[X] Impossible de comprendre tout le code
```

---

**4. Couplage technologique [VERROUILLE]**

**Problème : Une seule techno pour tout**

```
Monolithe en Python :

├─ API REST : Python/Flask [OK]
├─ Calculs mathématiques : Python [ATTENTION] (lent)
├─ Traitement d'images : Python [ATTENTION] (pas optimal)
├─ Machine Learning : Python [OK]
├─ Traitement temps réel : Python [X] (pas adapté)
└─ WebSocket : Python [ATTENTION] (limité)

Conséquences :
[X] Tu es coincé avec Python même si c'est pas optimal
[X] Tu ne peux pas utiliser Go pour le temps réel
[X] Tu ne peux pas utiliser Rust pour les perfs
```

**Avec microservices :**

```
├─ API REST : Python (optimal pour ça)
├─ Calculs : Go (plus rapide)
├─ Images : C++ (ultra rapide)
├─ ML : Python (optimal)
├─ Temps réel : Elixir (conçu pour ça)
└─ WebSocket : Node.js (parfait)

[OK] Chaque service dans la techno la plus adaptée
```

---

**5. Équipe bloquée [UTILISATEURS]**

**Problème : Plusieurs développeurs sur le même code**

```
Équipe de 10 développeurs sur un monolithe :

Lundi matin :
Dev 1 : "Je vais modifier users.py"
Dev 2 : "Moi aussi j'en ai besoin !"
Dev 1 : "Attends que j'aie fini"
Dev 2 : [!] (attend)

Mardi :
Dev 3 : "Je dois modifier la DB"
Dev 4 : "Moi aussi !"
Conflit de migration...

Mercredi :
Dev 5 : "Je push mon code"
Tests échouent (conflit avec le code de Dev 6)
Dev 5 + Dev 6 : 2 heures à débugger

Jeudi :
Merge de 5 branches -> [IMPACT] Conflits partout
Journée perdue à résoudre les conflits

Vendredi :
Déploiement : Tout le monde doit coordonner
"Attendez, je ne suis pas prêt !"
"Moi non plus !"
Déploiement repoussé au lundi...

Productivité : [BAISSE][BAISSE][BAISSE]
```

**Avec microservices :**

```
10 développeurs, 10 microservices :

Dev 1 : Service utilisateur (indépendant)
Dev 2 : Service produit (indépendant)
Dev 3 : Service commande (indépendant)
...

Chacun travaille sur son service
Zéro conflit
Déploiement indépendant
Productivité : [HAUSSE][HAUSSE][HAUSSE]
```

---

## [CONSTRUCTION] STRUCTURE D'UN MONOLITHE

### [DOSSIER] Organisation de base

**Structure simple (petit projet) :**

```
mon_app/
│
├─ app.py                    # Point d'entrée
├─ config.py                 # Configuration
├─ requirements.txt          # Dépendances
│
├─ models/                   # Modèles de données
│   ├─ __init__.py
│   ├─ user.py
│   ├─ product.py
│   └─ order.py
│
├─ routes/                   # Routes/Endpoints
│   ├─ __init__.py
│   ├─ user_routes.py
│   ├─ product_routes.py
│   └─ order_routes.py
│
├─ services/                 # Logique métier
│   ├─ __init__.py
│   ├─ user_service.py
│   ├─ product_service.py
│   └─ order_service.py
│
├─ utils/                    # Utilitaires
│   ├─ __init__.py
│   ├─ database.py
│   ├─ email.py
│   └─ validators.py
│
├─ static/                   # Fichiers statiques
│   ├─ css/
│   ├─ js/
│   └─ images/
│
└─ templates/                # Templates HTML
    ├─ base.html
    ├─ user/
    └─ product/
```

---

### [NOTE] Exemple de code complet

**1. Configuration (`config.py`) :**

```python
import os

class Config:
    """Configuration de l'application"""
    
    # Base de données
    DATABASE_URL = os.getenv('DATABASE_URL', 'sqlite:///app.db')
    
    # Secret key pour sessions
    SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-me')
    
    # Email
    SMTP_HOST = os.getenv('SMTP_HOST', 'smtp.gmail.com')
    SMTP_PORT = int(os.getenv('SMTP_PORT', 587))
    SMTP_USER = os.getenv('SMTP_USER', '')
    SMTP_PASSWORD = os.getenv('SMTP_PASSWORD', '')
    
    # Application
    DEBUG = os.getenv('DEBUG', 'False').lower() == 'true'
    PORT = int(os.getenv('PORT', 5000))

class DevelopmentConfig(Config):
    """Configuration de développement"""
    DEBUG = True
    DATABASE_URL = 'sqlite:///dev.db'

class ProductionConfig(Config):
    """Configuration de production"""
    DEBUG = False
    # En production, ces variables DOIVENT être définies
    DATABASE_URL = os.getenv('DATABASE_URL')
    SECRET_KEY = os.getenv('SECRET_KEY')

class TestConfig(Config):
    """Configuration de test"""
    TESTING = True
    DATABASE_URL = 'sqlite:///:memory:'

# Choisir la configuration selon l'environnement
config = {
    'development': DevelopmentConfig,
    'production': ProductionConfig,
    'test': TestConfig,
    'default': DevelopmentConfig
}
```

---

**2. Modèles (`models/user.py`) :**

```python
from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash
from utils.database import db

class User(db.Model):
    """Modèle utilisateur"""
    
    __tablename__ = 'users'
    
    # Colonnes
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    password_hash = db.Column(db.String(255), nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    is_active = db.Column(db.Boolean, default=True)
    
    # Relations
    orders = db.relationship('Order', backref='user', lazy=True)
    
    def set_password(self, password):
        """Hash le mot de passe"""
        self.password_hash = generate_password_hash(password)
    
    def check_password(self, password):
        """Vérifie le mot de passe"""
        return check_password_hash(self.password_hash, password)
    
    def to_dict(self):
        """Convertit en dictionnaire"""
        return {
            'id': self.id,
            'username': self.username,
            'email': self.email,
            'created_at': self.created_at.isoformat(),
            'is_active': self.is_active
        }
    
    def __repr__(self):
        return f'<User {self.username}>'
```

---

**3. Services (`services/user_service.py`) :**

```python
from models.user import User
from utils.database import db
from utils.email import send_email
from utils.validators import validate_email, validate_password

class UserService:
    """Service de gestion des utilisateurs"""
    
    @staticmethod
    def create_user(username, email, password):
        """
        Crée un nouvel utilisateur
        
        Args:
            username (str): Nom d'utilisateur
            email (str): Email
            password (str): Mot de passe
            
        Returns:
            User: Utilisateur créé
            
        Raises:
            ValueError: Si les données sont invalides
        """
        # Validation
        if not validate_email(email):
            raise ValueError("Email invalide")
        
        if not validate_password(password):
            raise ValueError("Mot de passe trop faible (min 8 caractères)")
        
        # Vérifier unicité
        if User.query.filter_by(username=username).first():
            raise ValueError("Nom d'utilisateur déjà pris")
        
        if User.query.filter_by(email=email).first():
            raise ValueError("Email déjà utilisé")
        
        # Créer l'utilisateur
        user = User(username=username, email=email)
        user.set_password(password)
        
        # Sauvegarder
        db.session.add(user)
        db.session.commit()
        
        # Envoyer email de bienvenue
        try:
            send_email(
                to=email,
                subject="Bienvenue !",
                body=f"Bonjour {username}, bienvenue sur notre plateforme !"
            )
        except Exception as e:
            # Log l'erreur mais ne fait pas échouer la création
            print(f"Erreur envoi email : {e}")
        
        return user
    
    @staticmethod
    def get_user_by_id(user_id):
        """Récupère un utilisateur par ID"""
        return User.query.get(user_id)
    
    @staticmethod
    def get_user_by_email(email):
        """Récupère un utilisateur par email"""
        return User.query.filter_by(email=email).first()
    
    @staticmethod
    def authenticate(email, password):
        """
        Authentifie un utilisateur
        
        Returns:
            User: Utilisateur si authentification réussie
            None: Si échec
        """
        user = UserService.get_user_by_email(email)
        
        if user and user.check_password(password):
            return user
        
        return None
    
    @staticmethod
    def update_user(user_id, **kwargs):
        """Met à jour un utilisateur"""
        user = User.query.get(user_id)
        
        if not user:
            raise ValueError("Utilisateur non trouvé")
        
        # Mise à jour des champs autorisés
        allowed_fields = ['username', 'email']
        for field, value in kwargs.items():
            if field in allowed_fields:
                setattr(user, field, value)
        
        db.session.commit()
        return user
    
    @staticmethod
    def delete_user(user_id):
        """Supprime un utilisateur (soft delete)"""
        user = User.query.get(user_id)
        
        if not user:
            raise ValueError("Utilisateur non trouvé")
        
        user.is_active = False
        db.session.commit()
        
        return True
```

---

**4. Routes (`routes/user_routes.py`) :**

```python
from flask import Blueprint, request, jsonify
from services.user_service import UserService
from utils.auth import login_required

# Créer le blueprint
user_bp = Blueprint('users', __name__, url_prefix='/api/users')

@user_bp.route('/', methods=['POST'])
def create_user():
    """
    Créer un utilisateur
    
    POST /api/users/
    Body: {"username": "john", "email": "john@example.com", "password": "secret123"}
    """
    try:
        data = request.get_json()
        
        # Validation des données requises
        if not all(k in data for k in ['username', 'email', 'password']):
            return jsonify({'error': 'Données manquantes'}), 400
        
        # Créer l'utilisateur
        user = UserService.create_user(
            username=data['username'],
            email=data['email'],
            password=data['password']
        )
        
        return jsonify({
            'message': 'Utilisateur créé avec succès',
            'user': user.to_dict()
        }), 201
        
    except ValueError as e:
        return jsonify({'error': str(e)}), 400
    except Exception as e:
        return jsonify({'error': 'Erreur serveur'}), 500

@user_bp.route('/<int:user_id>', methods=['GET'])
@login_required
def get_user(user_id):
    """
    Récupérer un utilisateur
    
    GET /api/users/123
    """
    user = UserService.get_user_by_id(user_id)
    
    if not user:
        return jsonify({'error': 'Utilisateur non trouvé'}), 404
    
    return jsonify(user.to_dict()), 200

@user_bp.route('/<int:user_id>', methods=['PUT'])
@login_required
def update_user(user_id):
    """
    Mettre à jour un utilisateur
    
    PUT /api/users/123
    Body: {"username": "new_username"}
    """
    try:
        data = request.get_json()
        user = UserService.update_user(user_id, **data)
        
        return jsonify({
            'message': 'Utilisateur mis à jour',
            'user': user.to_dict()
        }), 200
        
    except ValueError as e:
        return jsonify({'error': str(e)}), 404
    except Exception as e:
        return jsonify({'error': 'Erreur serveur'}), 500

@user_bp.route('/<int:user_id>', methods=['DELETE'])
@login_required
def delete_user(user_id):
    """
    Supprimer un utilisateur
    
    DELETE /api/users/123
    """
    try:
        UserService.delete_user(user_id)
        return jsonify({'message': 'Utilisateur supprimé'}), 200
        
    except ValueError as e:
        return jsonify({'error': str(e)}), 404
    except Exception as e:
        return jsonify({'error': 'Erreur serveur'}), 500

@user_bp.route('/login', methods=['POST'])
def login():
    """
    Connexion
    
    POST /api/users/login
    Body: {"email": "john@example.com", "password": "secret123"}
    """
    try:
        data = request.get_json()
        
        if not all(k in data for k in ['email', 'password']):
            return jsonify({'error': 'Email et mot de passe requis'}), 400
        
        user = UserService.authenticate(data['email'], data['password'])
        
        if not user:
            return jsonify({'error': 'Identifiants invalides'}), 401
        
        # Créer un token (JWT par exemple)
        # token = create_jwt_token(user.id)
        
        return jsonify({
            'message': 'Connexion réussie',
            'user': user.to_dict(),
            # 'token': token
        }), 200
        
    except Exception as e:
        return jsonify({'error': 'Erreur serveur'}), 500
```

---

**5. Application principale (`app.py`) :**

```python
import os
from flask import Flask
from flask_cors import CORS
from config import config
from utils.database import db, init_db
from routes.user_routes import user_bp
from routes.product_routes import product_bp
from routes.order_routes import order_bp

def create_app(config_name=None):
    """Factory pour créer l'application"""
    
    # Déterminer l'environnement
    if config_name is None:
        config_name = os.getenv('FLASK_ENV', 'default')
    
    # Créer l'app Flask
    app = Flask(__name__)
    
    # Charger la configuration
    app.config.from_object(config[config_name])
    
    # Initialiser les extensions
    db.init_app(app)
    CORS(app)
    
    # Enregistrer les blueprints (routes)
    app.register_blueprint(user_bp)
    app.register_blueprint(product_bp)
    app.register_blueprint(order_bp)
    
    # Route de santé
    @app.route('/health')
    def health():
        return {'status': 'ok'}, 200
    
    # Créer les tables
    with app.app_context():
        init_db()
    
    return app

if __name__ == '__main__':
    app = create_app()
    port = app.config.get('PORT', 5000)
    debug = app.config.get('DEBUG', False)
    
    print(f"[RAPIDE] Application démarrée sur le port {port}")
    app.run(host='0.0.0.0', port=port, debug=debug)
```

---

## [TEST] TESTER UN MONOLITHE

### Tests unitaires

```python
# tests/test_user_service.py
import unittest
from app import create_app
from utils.database import db
from services.user_service import UserService

class TestUserService(unittest.TestCase):
    """Tests du service utilisateur"""
    
    def setUp(self):
        """Exécuté avant chaque test"""
        self.app = create_app('test')
        self.client = self.app.test_client()
        self.app_context = self.app.app_context()
        self.app_context.push()
        db.create_all()
    
    def tearDown(self):
        """Exécuté après chaque test"""
        db.session.remove()
        db.drop_all()
        self.app_context.pop()
    
    def test_create_user_success(self):
        """Test : Création d'utilisateur réussie"""
        user = UserService.create_user(
            username='testuser',
            email='test@example.com',
            password='password123'
        )
        
        self.assertIsNotNone(user)
        self.assertEqual(user.username, 'testuser')
        self.assertEqual(user.email, 'test@example.com')
        self.assertTrue(user.check_password('password123'))
    
    def test_create_user_duplicate_email(self):
        """Test : Email déjà utilisé"""
        # Créer le premier utilisateur
        UserService.create_user('user1', 'test@example.com', 'pass123')
        
        # Tenter de créer un deuxième avec le même email
        with self.assertRaises(ValueError) as context:
            UserService.create_user('user2', 'test@example.com', 'pass456')
        
        self.assertIn('Email déjà utilisé', str(context.exception))
    
    def test_authenticate_success(self):
        """Test : Authentification réussie"""
        # Créer un utilisateur
        UserService.create_user('john', 'john@example.com', 'secret123')
        
        # Authentifier
        user = UserService.authenticate('john@example.com', 'secret123')
        
        self.assertIsNotNone(user)
        self.assertEqual(user.username, 'john')
    
    def test_authenticate_wrong_password(self):
        """Test : Mauvais mot de passe"""
        UserService.create_user('john', 'john@example.com', 'secret123')
        
        user = UserService.authenticate('john@example.com', 'wrong_password')
        
        self.assertIsNone(user)

if __name__ == '__main__':
    unittest.main()
```

**Exécuter les tests :**

```bash
# Tous les tests
python -m pytest tests/

# Tests spécifiques
python -m pytest tests/test_user_service.py

# Avec couverture
python -m pytest --cov=services tests/
```

---

### Tests d'intégration

```python
# tests/test_api.py
import unittest
import json
from app import create_app
from utils.database import db

class TestUserAPI(unittest.TestCase):
    """Tests de l'API utilisateur"""
    
    def setUp(self):
        self.app = create_app('test')
        self.client = self.app.test_client()
        self.app_context = self.app.app_context()
        self.app_context.push()
        db.create_all()
    
    def tearDown(self):
        db.session.remove()
        db.drop_all()
        self.app_context.pop()
    
    def test_create_user_endpoint(self):
        """Test : Endpoint de création d'utilisateur"""
        response = self.client.post(
            '/api/users/',
            data=json.dumps({
                'username': 'testuser',
                'email': 'test@example.com',
                'password': 'password123'
            }),
            content_type='application/json'
        )
        
        self.assertEqual(response.status_code, 201)
        data = json.loads(response.data)
        self.assertIn('user', data)
        self.assertEqual(data['user']['username'], 'testuser')
    
    def test_login_endpoint(self):
        """Test : Endpoint de connexion"""
        # Créer un utilisateur d'abord
        self.client.post(
            '/api/users/',
            data=json.dumps({
                'username': 'john',
                'email': 'john@example.com',
                'password': 'secret123'
            }),
            content_type='application/json'
        )
        
        # Se connecter
        response = self.client.post(
            '/api/users/login',
            data=json.dumps({
                'email': 'john@example.com',
                'password': 'secret123'
            }),
            content_type='application/json'
        )
        
        self.assertEqual(response.status_code, 200)
        data = json.loads(response.data)
        self.assertEqual(data['message'], 'Connexion réussie')
    
    def test_get_user_endpoint(self):
        """Test : Récupération d'utilisateur"""
        # Créer un utilisateur
        create_response = self.client.post(
            '/api/users/',
            data=json.dumps({
                'username': 'jane',
                'email': 'jane@example.com',
                'password': 'pass123'
            }),
            content_type='application/json'
        )
        
        user_id = json.loads(create_response.data)['user']['id']
        
        # Récupérer l'utilisateur
        response = self.client.get(f'/api/users/{user_id}')
        
        self.assertEqual(response.status_code, 200)
        data = json.loads(response.data)
        self.assertEqual(data['username'], 'jane')
```

---

## [RAPIDE] DÉPLOIEMENT D'UN MONOLITHE

### Déploiement simple (serveur unique)

**1. Préparer l'environnement**

```bash
# Sur le serveur
sudo apt update
sudo apt install python3 python3-pip python3-venv nginx

# Créer un utilisateur dédié
sudo useradd -m -s /bin/bash appuser
sudo su - appuser
```

---

**2. Déployer l'application**

```bash
# Cloner le code
git clone https://github.com/ton-username/mon-app.git
cd mon-app

# Créer un environnement virtuel
python3 -m venv venv
source venv/bin/activate

# Installer les dépendances
pip install -r requirements.txt

# Configurer les variables d'environnement
cat > .env << EOF
FLASK_ENV=production
DATABASE_URL=postgresql://user:password@localhost/myapp
SECRET_KEY=$(python -c 'import secrets; print(secrets.token_hex(32))')
EOF

# Initialiser la base de données
flask db upgrade

# Tester le démarrage
python app.py
```

---

**3. Configurer le service systemd**

```bash
# Créer le fichier service
sudo nano /etc/systemd/system/monapp.service
```

**Contenu :**

```ini
[Unit]
Description=Mon Application Monolithe
After=network.target

[Service]
Type=simple
User=appuser
WorkingDirectory=/home/appuser/mon-app
Environment="PATH=/home/appuser/mon-app/venv/bin"
EnvironmentFile=/home/appuser/mon-app/.env
ExecStart=/home/appuser/mon-app/venv/bin/python app.py
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
```

**Activer le service :**

```bash
sudo systemctl daemon-reload
sudo systemctl enable monapp
sudo systemctl start monapp
sudo systemctl status monapp
```

---

**4. Configurer Nginx (reverse proxy)**

```bash
sudo nano /etc/nginx/sites-available/monapp
```

**Contenu :**

```nginx
server {
    listen 80;
    server_name monapp.example.com;

    location / {
        proxy_pass http://127.0.0.1:5000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location /static/ {
        alias /home/appuser/mon-app/static/;
    }
}
```

**Activer :**

```bash
sudo ln -s /etc/nginx/sites-available/monapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx
```

---

**5. SSL avec Let's Encrypt**

```bash
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d monapp.example.com
```

**[OK] Application déployée et accessible en HTTPS ! [BRAVO]**

---

### Déploiement avec Docker

**1. Créer le Dockerfile**

```dockerfile
FROM python:3.11-slim

WORKDIR /app

# Installer les dépendances système
RUN apt-get update && apt-get install -y \
    gcc \
    postgresql-client \
    && rm -rf /var/lib/apt/lists/*

# Copier les requirements
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copier le code
COPY . .

# Variables d'environnement
ENV FLASK_ENV=production
ENV PYTHONUNBUFFERED=1

# Exposer le port
EXPOSE 5000

# Commande de démarrage
CMD ["python", "app.py"]
```

---

**2. Créer docker-compose.yml**

```yaml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "5000:5000"
    environment:
      - FLASK_ENV=production
      - DATABASE_URL=postgresql://user:password@db:5432/myapp
      - SECRET_KEY=${SECRET_KEY}
    depends_on:
      - db
    volumes:
      - ./logs:/app/logs
    restart: unless-stopped

  db:
    image: postgres:15
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=myapp
    volumes:
      - postgres_data:/var/lib/postgresql/data
    restart: unless-stopped

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./ssl:/etc/nginx/ssl:ro
    depends_on:
      - app
    restart: unless-stopped

volumes:
  postgres_data:
```

---

**3. Déployer**

```bash
# Build et démarrage
docker-compose up -d

# Voir les logs
docker-compose logs -f

# Migrations
docker-compose exec app flask db upgrade

# Redémarrage
docker-compose restart app

# Arrêt
docker-compose down
```

---

## [GRAPHIQUE] MONITORING D'UN MONOLITHE

### Logs

**Configuration des logs :**

```python
# app.py
import logging
from logging.handlers import RotatingFileHandler

def setup_logging(app):
    """Configure les logs"""
    
    # Format des logs
    formatter = logging.Formatter(
        '[%(asctime)s] %(levelname)s in %(module)s: %(message)s'
    )
    
    # Handler fichier (rotation automatique)
    file_handler = RotatingFileHandler(
        'logs/app.log',
        maxBytes=10 * 1024 * 1024,  # 10 MB
        backupCount=10
    )
    file_handler.setFormatter(formatter)
    file_handler.setLevel(logging.INFO)
    
    # Handler console
    console_handler = logging.StreamHandler()
    console_handler.setFormatter(formatter)
    console_handler.setLevel(logging.DEBUG if app.config['DEBUG'] else logging.INFO)
    
    # Ajouter à l'app
    app.logger.addHandler(file_handler)
    app.logger.addHandler(console_handler)
    app.logger.setLevel(logging.INFO)
    
    return app

# Utiliser dans create_app
app = create_app()
app = setup_logging(app)
```

**Utilisation :**

```python
from flask import current_app

@user_bp.route('/', methods=['POST'])
def create_user():
    current_app.logger.info(f"Tentative de création d'utilisateur")
    
    try:
        # ...
        current_app.logger.info(f"Utilisateur créé : {user.id}")
        return jsonify(user.to_dict()), 201
        
    except Exception as e:
        current_app.logger.error(f"Erreur création utilisateur : {e}")
        return jsonify({'error': 'Erreur serveur'}), 500
```

---

### Métriques avec Prometheus

**Installation :**

```bash
pip install prometheus-flask-exporter
```

**Configuration :**

```python
from prometheus_flask_exporter import PrometheusMetrics

app = create_app()

# Activer les métriques
metrics = PrometheusMetrics(app)

# Métriques personnalisées
request_duration = metrics.histogram(
    'request_duration_seconds',
    'Request duration in seconds',
    labels={'endpoint': lambda: request.endpoint}
)

# Compteur d'erreurs
error_counter = metrics.counter(
    'app_errors_total',
    'Total number of errors',
    labels={'error_type': lambda: 'unknown'}
)
```

**Endpoint des métriques :**

```
GET /metrics

# TYPE request_duration_seconds histogram
request_duration_seconds_bucket{endpoint="/api/users/",le="0.005"} 45
request_duration_seconds_bucket{endpoint="/api/users/",le="0.01"} 120
request_duration_seconds_sum{endpoint="/api/users/"} 15.3
request_duration_seconds_count{endpoint="/api/users/"} 150
```

---

### Dashboard avec Grafana

**docker-compose.yml (ajout monitoring) :**

```yaml
services:
  # ... app, db, nginx ...

  prometheus:
    image: prom/prometheus
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
    restart: unless-stopped

  grafana:
    image: grafana/grafana
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
    volumes:
      - grafana_data:/var/lib/grafana
    depends_on:
      - prometheus
    restart: unless-stopped

volumes:
  prometheus_data:
  grafana_data:
```

**Configuration Prometheus (`prometheus.yml`) :**

```yaml
global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'mon-app'
    static_configs:
      - targets: ['app:5000']
```

**Accéder à Grafana :**

```
http://localhost:3000
Login : admin / admin

Ajouter Prometheus comme data source
Créer des dashboards pour :
- Nombre de requêtes
- Temps de réponse
- Taux d'erreurs
- Utilisation CPU/RAM
```

---

## [SYNC] MIGRATION DEPUIS UN MONOLITHE

### Quand migrer ?

**Signes qu'il est temps de migrer :**

```
[X] Déploiements trop longs (> 30 minutes)
[X] Équipe > 10 développeurs qui se marchent dessus
[X] Base de code > 200,000 lignes
[X] Temps de build > 15 minutes
[X] Impossible de scaler des parties spécifiques
[X] Couplage technologique limitant
[X] Modifications risquées (peur de casser)
```

---

### Stratégie de migration

**Approche progressive (Strangler Fig Pattern) :**

```
Étape 1 : Identifier les modules indépendants
├─ Module authentification
├─ Module notifications
└─ Module reporting

Étape 2 : Extraire un module à la fois
Monolithe
├─ [Authentification] -> Extrait en microservice
├─ [Notifications] -> Extrait en microservice
├─ Produits
├─ Commandes
└─ Paiement

Étape 3 : Connecter via API
Monolithe <--> API Gateway <--> Microservices

Étape 4 : Continuer progressivement
Monolithe réduit
├─ Produits -> Extrait
├─ Commandes -> Extrait
└─ Paiement -> Extrait

Étape 5 : Éventuellement, plus de monolithe
Microservices uniquement
```

**Durée typique : 6-18 mois** (selon la taille)

---

## [DOCS] EXEMPLES RÉELS

### Shopify (1.7M lignes de Ruby)

**Faits :**
- Monolithe géant depuis 2006
- 1.7 million de lignes de code Ruby
- Des milliers de boutiques en ligne
- Équipe de 500+ développeurs

**Pourquoi ils restent en monolithe ?**

```
[OK] Transactions complexes (paiement, stock, commandes)
[OK] Consistance des données critique
[OK] Expertise Ruby très forte
[OK] Outillage excellent (tests, CI/CD)
[OK] Modularisation interne bien faite
```

**Ils ont quand même extrait certains services :**
- Recherche (Elasticsearch)
- Traitement d'images (service séparé)
- Analytics (data pipeline séparé)

**Leçon : Un monolithe bien fait peut aller TRÈS loin.**

---

### GitHub (Rails monolithe)

**Faits :**
- Monolithe Rails depuis 2008
- Des millions de repositories
- 100+ millions d'utilisateurs
- Équipe de développement massive

**Évolution :**
- 2008-2016 : Monolithe pur
- 2016-2020 : Extraction progressive de services
  - Authentification
  - Storage (Git LFS)
  - CI/CD (Actions)
- 2020+ : Hybride (monolithe + microservices ciblés)

**Leçon : Migration progressive sur plusieurs années.**

---

## [OBJECTIF] EXERCICES PRATIQUES

### Exercice 1 : Créer un blog monolithique **

**Objectif : Créer un blog avec :**
- Authentification
- CRUD articles
- Commentaires
- Tags

**Structure recommandée :**

```
blog/
├─ models/
│   ├─ user.py
│   ├─ article.py
│   ├─ comment.py
│   └─ tag.py
├─ services/
│   ├─ auth_service.py
│   ├─ article_service.py
│   └─ comment_service.py
├─ routes/
│   ├─ auth_routes.py
│   ├─ article_routes.py
│   └─ comment_routes.py
└─ app.py
```

**Fonctionnalités :**
1. S'inscrire / Se connecter
2. Créer un article
3. Lister les articles
4. Voir un article + ses commentaires
5. Commenter un article
6. Ajouter des tags

---

### Exercice 2 : API E-commerce ***

**Objectif : Créer une API de e-commerce**

**Fonctionnalités :**
- Gestion produits (CRUD)
- Panier d'achat
- Commandes
- Paiement (simulation)
- Historique des commandes

**Contraintes :**
- Transactions atomiques
- Validation des stocks
- Calcul de prix avec remises
- Tests unitaires et d'intégration

---

### Exercice 3 : Refactoring d'un monolithe ****

**Objectif : Refactorer un monolithe mal organisé**

**Code de départ (tout dans un fichier) :**

```python
# bad_app.py (1000 lignes)
from flask import Flask, request, jsonify
import sqlite3

app = Flask(__name__)

@app.route('/users', methods=['POST'])
def create_user():
    data = request.json
    conn = sqlite3.connect('db.sqlite')
    cursor = conn.cursor()
    cursor.execute("INSERT INTO users ...")
    conn.commit()
    # ... 50 lignes de code mélangé
    # Email, validation, logging, etc.
    return jsonify({'ok': True})

# ... 20 autres routes similaires
```

**Mission :**
1. Séparer en couches (models, services, routes)
2. Extraire la logique métier
3. Créer des services réutilisables
4. Ajouter des tests
5. Améliorer la gestion d'erreurs

---

## [BRAVO] CONCLUSION

### Ce que tu as appris

**Concepts :**
- [OK] Qu'est-ce qu'un monolithe
- [OK] Avantages et inconvénients
- [OK] Quand l'utiliser
- [OK] Comment le structurer

**Pratique :**
- [OK] Organisation du code
- [OK] Déploiement
- [OK] Tests
- [OK] Monitoring

**Stratégie :**
- [OK] Quand migrer
- [OK] Comment migrer progressivement

---

### Points clés à retenir

```
[OBJECTIF] Le monolithe n'est PAS une mauvaise architecture
[OBJECTIF] C'est le meilleur choix pour débuter
[OBJECTIF] Beaucoup de grandes apps sont des monolithes réussis
[OBJECTIF] La simplicité est une qualité
[OBJECTIF] Ne pas sur-ingénierer prématurément
```

---

### Prochaines étapes

```
1. Code un projet monolithique simple
2. Déploie-le sur un serveur
3. Ajoute des features progressivement
4. Observe les limites du monolithe
5. Passe à architecture_mvc.txt
```

**[RAPIDE] Bon courage pour tes projets monolithiques ! [RAPIDE]**

═══════════════════════════════════════════════════════════════
FIN DU FICHIER : architecture_monolithique.txt
═══════════════════════════════════════════════════════════════

# [DESIGN] ARCHITECTURE MVC (MODÈLE-VUE-CONTRÔLEUR) - GUIDE ULTRA DÉTAILLÉ

## * INTRODUCTION

### Qu'est-ce que MVC ?

**MVC = Model-View-Controller**

C'est un **pattern architectural** qui sépare une application en **3 composants principaux** :

```
[GRAPHIQUE] MODÈLE (Model)      -> Les données et la logique métier
[DESIGN] VUE (View)          -> L'interface utilisateur (ce que tu vois)
[VIDEO_GAME] CONTRÔLEUR (Controller) -> Le chef d'orchestre (gère les requêtes)
```

**Analogie : Restaurant [FORK_AND_KNIFE_WITH_PLATE]**

```
[PERSONNE][COOKING] CUISINE (Model)
   ├─ Ingrédients (données)
   ├─ Recettes (logique métier)
   └─ Préparation des plats

[FORK_AND_KNIFE_WITH_PLATE] SALLE À MANGER (View)
   ├─ Présentation des plats
   ├─ Décoration
   └─ Expérience client

[PERSONNE][PRO] SERVEUR (Controller)
   ├─ Prend les commandes du client
   ├─ Transmet à la cuisine
   ├─ Ramène les plats
   └─ Gère les interactions

Client (Utilisateur) -> Serveur -> Cuisine -> Serveur -> Client
```

**Le client (utilisateur) ne va JAMAIS directement en cuisine !**
**Il passe toujours par le serveur (contrôleur).**

---

### [GRAPHIQUE] Diagramme MVC

```
┌──────────────┐
│ UTILISATEUR  │
└──────┬───────┘
       │ 1. Requête
       v
┌──────────────────┐
│  CONTRÔLEUR      │
│  (Controller)    │
└──────┬───────────┘
       │ 2. Demande données
       v
┌──────────────────┐
│  MODÈLE          │ <- 3. Retourne données
│  (Model)         │
└──────┬───────────┘
       │ 4. Données
       v
┌──────────────────┐
│  VUE             │
│  (View)          │
└──────┬───────────┘
       │ 5. HTML généré
       v
┌──────────────────┐
│  UTILISATEUR     │
└──────────────────┘
```

**Flux détaillé :**

```
1. L'utilisateur fait une action (clic, formulaire, etc.)
   v
2. Le Contrôleur reçoit la requête
   v
3. Le Contrôleur demande des données au Modèle
   v
4. Le Modèle accède à la base de données et retourne les données
   v
5. Le Contrôleur passe les données à la Vue
   v
6. La Vue génère le HTML avec les données
   v
7. Le HTML est envoyé à l'utilisateur
```

---

## [REFLEXION] POURQUOI UTILISER MVC ?

### [OK] Avantages

**1. Séparation des responsabilités [OBJECTIF]**

**Sans MVC (tout mélangé) :**

```python
# [X] Code spaghetti - TOUT dans une seule fonction
@app.route('/users/<user_id>')
def show_user(user_id):
    # Requête DB directement dans la route
    conn = sqlite3.connect('database.db')
    cursor = conn.cursor()
    cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
    user = cursor.fetchone()
    conn.close()
    
    # Validation mélangée
    if not user:
        return "Erreur 404"
    
    # Calculs métier mélangés
    age = 2024 - user[3]  # Calcul de l'âge
    
    # HTML généré dans le code
    html = f"""
    <html>
        <body>
            <h1>{user[1]}</h1>
            <p>Email: {user[2]}</p>
            <p>Age: {age}</p>
        </body>
    </html>
    """
    return html
```

**Problèmes :**
- [X] Impossible de réutiliser la logique
- [X] Difficile à tester
- [X] HTML mélangé au code Python
- [X] Modification du design = Modifier le code Python
- [X] Pas de réutilisation de templates

---

**Avec MVC (séparé) :**

```python
# [OK] MODÈLE (models/user.py)
class User:
    @staticmethod
    def find(user_id):
        """Récupère un utilisateur par ID"""
        conn = sqlite3.connect('database.db')
        cursor = conn.cursor()
        cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
        user = cursor.fetchone()
        conn.close()
        return user
    
    def calculate_age(self, birth_year):
        """Calcule l'âge"""
        return 2024 - birth_year

# [OK] CONTRÔLEUR (controllers/user_controller.py)
@app.route('/users/<user_id>')
def show_user(user_id):
    """Contrôleur : Orchestre les actions"""
    # Demande au Modèle
    user = User.find(user_id)
    
    # Vérifications
    if not user:
        abort(404)
    
    # Prépare les données
    age = User.calculate_age(user['birth_year'])
    
    # Passe à la Vue
    return render_template('user/show.html', user=user, age=age)

# [OK] VUE (templates/user/show.html)
<html>
    <body>
        <h1>{{ user.name }}</h1>
        <p>Email: {{ user.email }}</p>
        <p>Age: {{ age }}</p>
    </body>
</html>
```

**Avantages :**
- [OK] Logique métier isolée (Modèle)
- [OK] Présentation séparée (Vue)
- [OK] Orchestration claire (Contrôleur)
- [OK] Facile à tester chaque partie
- [OK] Designer peut modifier la Vue sans toucher au code

---

**2. Réutilisabilité [SYNC]**

**Modèle réutilisable partout :**

```python
# Le même modèle User peut être utilisé par :

# Contrôleur Web
@app.route('/users/<user_id>')
def web_show_user(user_id):
    user = User.find(user_id)  # <- Réutilisation
    return render_template('user.html', user=user)

# API REST
@app.route('/api/users/<user_id>')
def api_get_user(user_id):
    user = User.find(user_id)  # <- Même code !
    return jsonify(user)

# CLI (ligne de commande)
def cli_show_user(user_id):
    user = User.find(user_id)  # <- Encore le même !
    print(f"Name: {user['name']}")

# Tests
def test_user():
    user = User.find(1)  # <- Toujours le même !
    assert user is not None
```

**Pas de duplication de code ! [OK]**

---

**3. Testabilité [TEST]**

**Tester chaque partie séparément :**

```python
# Test du MODÈLE (isolation complète)
def test_user_model():
    user = User.find(1)
    assert user['name'] == 'John'
    
    age = User.calculate_age(1990)
    assert age == 34

# Test du CONTRÔLEUR (avec mock du modèle)
def test_user_controller():
    with patch('User.find') as mock_find:
        mock_find.return_value = {'name': 'John', 'birth_year': 1990}
        
        response = client.get('/users/1')
        assert response.status_code == 200
        assert b'John' in response.data

# Test de la VUE (avec données factices)
def test_user_view():
    html = render_template('user/show.html', 
                          user={'name': 'John'},
                          age=34)
    assert 'John' in html
    assert '34' in html
```

**Chaque composant se teste indépendamment ! [OK]**

---

**4. Maintenance facilitée [OUTIL]**

**Scénarios de modification :**

**Scénario 1 : Changer le design**

```
Sans MVC :
[X] Modifier tout le code Python qui génère le HTML
[X] Risque de casser la logique métier
[X] Temps : 2-3 jours

Avec MVC :
[OK] Modifier uniquement les fichiers .html (Vue)
[OK] Zéro risque pour la logique
[OK] Temps : 2-3 heures
```

---

**Scénario 2 : Changer de base de données**

```
Sans MVC :
[X] Modifier TOUS les fichiers qui font des requêtes SQL
[X] 50+ fichiers à modifier
[X] Tests de régression massifs

Avec MVC :
[OK] Modifier uniquement les Modèles
[OK] Les Contrôleurs et Vues ne changent pas
[OK] Tests ciblés sur les Modèles
```

---

**Scénario 3 : Ajouter une API**

```
Sans MVC :
[X] Réécrire toute la logique pour l'API
[X] Duplication de code

Avec MVC :
[OK] Créer de nouveaux Contrôleurs (API)
[OK] Réutiliser les Modèles existants
[OK] Pas de Vue (JSON à la place)
```

---

**5. Collaboration d'équipe [UTILISATEURS]**

**Division du travail :**

```
[PERSONNE][CODE] Développeur Backend
   ├─ Travaille sur les Modèles
   ├─ Travaille sur les Contrôleurs
   └─ API, logique métier, DB

[DESIGN] Designer / Intégrateur
   ├─ Travaille sur les Vues
   ├─ HTML, CSS, JavaScript
   └─ Expérience utilisateur

[SYNC] Pas de conflits !
```

**Exemple de workflow :**

```
Jour 1 :
Backend : Crée les Modèles User, Product
Designer : Crée les maquettes HTML/CSS

Jour 2 :
Backend : Crée les Contrôleurs
Designer : Intègre le design dans les templates

Jour 3 :
Backend : Connecte Contrôleurs aux Modèles
Designer : Peaufine les animations CSS

Jour 4 :
Intégration : Backend + Design = Application complète [OK]
```

---

### [X] Inconvénients

**1. Complexité initiale [HAUSSE]**

**Pour un projet très simple :**

```python
# Sans MVC - 1 fichier, 20 lignes
@app.route('/')
def hello():
    return "Hello World"

# Avec MVC - 5 fichiers, structure complète
models/
controllers/
views/
config/
app.py
```

**Pour 1 page statique, MVC est overkill.**

---

**2. Courbe d'apprentissage [ALARM_CLOCK]**

**Concepts à maîtriser :**

```
Débutant complet :
├─ Comprendre la séparation M-V-C
├─ Apprendre un framework (Flask, Django, Rails)
├─ Templating (Jinja, ERB, Blade)
├─ ORM (SQLAlchemy, ActiveRecord)
└─ Routing

Temps d'apprentissage : 2-4 semaines
```

**Versus monolithe simple : 1-2 jours**

---

**3. Overhead de fichiers [DOSSIER]**

**Structure MVC typique :**

```
mon_app/
├─ models/          (10+ fichiers)
├─ views/           (50+ templates)
├─ controllers/     (15+ fichiers)
├─ assets/
├─ config/
├─ migrations/
└─ tests/

Total : 100+ fichiers pour une app moyenne
```

**Versus monolithe simple : 5-10 fichiers**

---

## [CONSTRUCTION] COMPOSANTS MVC EN DÉTAIL

### [GRAPHIQUE] 1. LE MODÈLE (Model)

**Rôle : Gérer les données et la logique métier**

**Responsabilités :**

```
[OK] Accéder à la base de données
[OK] Valider les données
[OK] Logique métier (calculs, règles)
[OK] Relations entre entités
[X] PAS de HTML
[X] PAS de gestion des requêtes HTTP
```

**Exemple complet de Modèle :**

```python
# models/user.py
from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash
from sqlalchemy import Column, Integer, String, Boolean, DateTime
from database import db

class User(db.Model):
    """Modèle utilisateur"""
    
    __tablename__ = 'users'
    
    # ─────────────────────────────────────────────────────────
    # ATTRIBUTS (Colonnes de la base de données)
    # ─────────────────────────────────────────────────────────
    id = Column(Integer, primary_key=True)
    username = Column(String(80), unique=True, nullable=False)
    email = Column(String(120), unique=True, nullable=False)
    password_hash = Column(String(255), nullable=False)
    first_name = Column(String(50))
    last_name = Column(String(50))
    birth_year = Column(Integer)
    is_active = Column(Boolean, default=True)
    is_admin = Column(Boolean, default=False)
    created_at = Column(DateTime, default=datetime.utcnow)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    
    # Relations
    posts = db.relationship('Post', backref='author', lazy=True)
    comments = db.relationship('Comment', backref='author', lazy=True)
    
    # ─────────────────────────────────────────────────────────
    # MÉTHODES D'INSTANCE (sur un utilisateur spécifique)
    # ─────────────────────────────────────────────────────────
    
    def set_password(self, password):
        """Hash et stocke le mot de passe"""
        self.password_hash = generate_password_hash(password)
    
    def check_password(self, password):
        """Vérifie le mot de passe"""
        return check_password_hash(self.password_hash, password)
    
    def get_full_name(self):
        """Retourne le nom complet"""
        if self.first_name and self.last_name:
            return f"{self.first_name} {self.last_name}"
        return self.username
    
    def calculate_age(self):
        """Calcule l'âge de l'utilisateur"""
        if self.birth_year:
            return datetime.now().year - self.birth_year
        return None
    
    def is_adult(self):
        """Vérifie si l'utilisateur est majeur"""
        age = self.calculate_age()
        return age >= 18 if age else False
    
    def can_post(self):
        """Vérifie si l'utilisateur peut poster"""
        return self.is_active and self.is_adult()
    
    def deactivate(self):
        """Désactive le compte utilisateur"""
        self.is_active = False
        db.session.commit()
    
    # ─────────────────────────────────────────────────────────
    # MÉTHODES DE CLASSE (sur la table entière)
    # ─────────────────────────────────────────────────────────
    
    @classmethod
    def find_by_email(cls, email):
        """Trouve un utilisateur par email"""
        return cls.query.filter_by(email=email).first()
    
    @classmethod
    def find_by_username(cls, username):
        """Trouve un utilisateur par username"""
        return cls.query.filter_by(username=username).first()
    
    @classmethod
    def create(cls, username, email, password, **kwargs):
        """Crée un nouvel utilisateur"""
        user = cls(username=username, email=email, **kwargs)
        user.set_password(password)
        db.session.add(user)
        db.session.commit()
        return user
    
    @classmethod
    def get_active_users(cls):
        """Retourne tous les utilisateurs actifs"""
        return cls.query.filter_by(is_active=True).all()
    
    @classmethod
    def get_admins(cls):
        """Retourne tous les administrateurs"""
        return cls.query.filter_by(is_admin=True, is_active=True).all()
    
    @classmethod
    def search(cls, query):
        """Recherche des utilisateurs"""
        pattern = f"%{query}%"
        return cls.query.filter(
            (cls.username.like(pattern)) |
            (cls.email.like(pattern)) |
            (cls.first_name.like(pattern)) |
            (cls.last_name.like(pattern))
        ).all()
    
    # ─────────────────────────────────────────────────────────
    # VALIDATIONS
    # ─────────────────────────────────────────────────────────
    
    def validate(self):
        """Valide les données de l'utilisateur"""
        errors = []
        
        # Username
        if not self.username or len(self.username) < 3:
            errors.append("Username trop court (min 3 caractères)")
        
        if not self.username.isalnum():
            errors.append("Username doit être alphanumérique")
        
        # Email
        if not self.email or '@' not in self.email:
            errors.append("Email invalide")
        
        # Age
        if self.birth_year:
            age = self.calculate_age()
            if age < 13:
                errors.append("Vous devez avoir au moins 13 ans")
        
        return errors
    
    # ─────────────────────────────────────────────────────────
    # SÉRIALISATION (pour JSON/API)
    # ─────────────────────────────────────────────────────────
    
    def to_dict(self, include_email=False):
        """Convertit en dictionnaire"""
        data = {
            'id': self.id,
            'username': self.username,
            'full_name': self.get_full_name(),
            'created_at': self.created_at.isoformat(),
            'is_active': self.is_active
        }
        
        if include_email:
            data['email'] = self.email
        
        return data
    
    def to_json(self, include_email=False):
        """Convertit en JSON"""
        import json
        return json.dumps(self.to_dict(include_email))
    
    # ─────────────────────────────────────────────────────────
    # REPRÉSENTATION
    # ─────────────────────────────────────────────────────────
    
    def __repr__(self):
        return f'<User {self.username}>'
    
    def __str__(self):
        return self.get_full_name()
```

**Points clés :**

```
[OK] Toute la logique métier est dans le Modèle
[OK] Pas de dépendance aux Contrôleurs ou Vues
[OK] Réutilisable dans n'importe quel contexte
[OK] Testable facilement
```

---

### [DESIGN] 2. LA VUE (View)

**Rôle : Présenter les données à l'utilisateur**

**Responsabilités :**

```
[OK] Afficher les données (HTML)
[OK] Mise en forme visuelle (CSS)
[OK] Interactivité (JavaScript)
[OK] Templates réutilisables
[X] PAS de logique métier complexe
[X] PAS d'accès direct à la base de données
```

**Exemple complet de Vue :**

```html
<!-- views/users/show.html -->
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{ user.username }} - Profil</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
    <!-- Header -->
    {% include 'partials/header.html' %}
    
    <!-- Contenu principal -->
    <main class="container">
        <div class="profile-card">
            <!-- Photo de profil -->
            <div class="profile-avatar">
                <img src="{{ user.avatar_url or url_for('static', filename='img/default-avatar.png') }}" 
                     alt="{{ user.username }}">
            </div>
            
            <!-- Informations -->
            <div class="profile-info">
                <h1>{{ user.get_full_name() }}</h1>
                <p class="username">@{{ user.username }}</p>
                
                {% if user.email and show_email %}
                <p class="email">
                    <i class="icon-email"></i>
                    {{ user.email }}
                </p>
                {% endif %}
                
                {% if user.calculate_age() %}
                <p class="age">
                    <i class="icon-calendar"></i>
                    {{ user.calculate_age() }} ans
                </p>
                {% endif %}
                
                <!-- Badges -->
                <div class="badges">
                    {% if user.is_admin %}
                    <span class="badge badge-admin">Administrateur</span>
                    {% endif %}
                    
                    {% if user.is_adult() %}
                    <span class="badge badge-verified">Vérifié</span>
                    {% endif %}
                    
                    {% if not user.is_active %}
                    <span class="badge badge-inactive">Inactif</span>
                    {% endif %}
                </div>
                
                <!-- Statistiques -->
                <div class="stats">
                    <div class="stat">
                        <span class="stat-value">{{ user.posts|length }}</span>
                        <span class="stat-label">Publications</span>
                    </div>
                    <div class="stat">
                        <span class="stat-value">{{ user.comments|length }}</span>
                        <span class="stat-label">Commentaires</span>
                    </div>
                    <div class="stat">
                        <span class="stat-value">{{ followers_count }}</span>
                        <span class="stat-label">Abonnés</span>
                    </div>
                </div>
                
                <!-- Actions -->
                <div class="actions">
                    {% if current_user.id == user.id %}
                        <!-- Actions pour l'utilisateur lui-même -->
                        <a href="{{ url_for('users.edit', user_id=user.id) }}" 
                           class="btn btn-primary">
                            Modifier le profil
                        </a>
                        <a href="{{ url_for('users.settings', user_id=user.id) }}" 
                           class="btn btn-secondary">
                            Paramètres
                        </a>
                    {% else %}
                        <!-- Actions pour les autres utilisateurs -->
                        {% if is_following %}
                        <button onclick="unfollowUser({{ user.id }})" 
                                class="btn btn-secondary">
                            Ne plus suivre
                        </button>
                        {% else %}
                        <button onclick="followUser({{ user.id }})" 
                                class="btn btn-primary">
                            Suivre
                        </button>
                        {% endif %}
                        
                        <a href="{{ url_for('messages.new', recipient_id=user.id) }}" 
                           class="btn btn-outline">
                            Message
                        </a>
                    {% endif %}
                </div>
            </div>
        </div>
        
        <!-- Publications récentes -->
        <section class="user-posts">
            <h2>Publications récentes</h2>
            
            {% if user.posts %}
                <div class="posts-grid">
                    {% for post in user.posts[:6] %}
                    <article class="post-card">
                        <h3>
                            <a href="{{ url_for('posts.show', post_id=post.id) }}">
                                {{ post.title }}
                            </a>
                        </h3>
                        <p class="post-excerpt">
                            {{ post.content[:150] }}...
                        </p>
                        <div class="post-meta">
                            <span class="post-date">{{ post.created_at|humanize }}</span>
                            <span class="post-comments">
                                {{ post.comments|length }} commentaires
                            </span>
                        </div>
                    </article>
                    {% endfor %}
                </div>
                
                {% if user.posts|length > 6 %}
                <a href="{{ url_for('users.posts', user_id=user.id) }}" 
                   class="btn btn-link">
                    Voir toutes les publications ->
                </a>
                {% endif %}
            {% else %}
                <p class="empty-state">
                    Aucune publication pour le moment.
                </p>
            {% endif %}
        </section>
    </main>
    
    <!-- Footer -->
    {% include 'partials/footer.html' %}
    
    <!-- Scripts -->
    <script src="{{ url_for('static', filename='js/profile.js') }}"></script>
</body>
</html>
```

**Template partiel (réutilisable) :**

```html
<!-- views/partials/user_card.html -->
<div class="user-card">
    <img src="{{ user.avatar_url or url_for('static', filename='img/default-avatar.png') }}" 
         alt="{{ user.username }}"
         class="user-avatar">
    <div class="user-info">
        <h3>{{ user.get_full_name() }}</h3>
        <p class="username">@{{ user.username }}</p>
        {% if show_stats %}
        <p class="stats">
            {{ user.posts|length }} publications
        </p>
        {% endif %}
    </div>
</div>
```

**Utilisation du partiel :**

```html
<!-- Dans n'importe quelle vue -->
{% include 'partials/user_card.html' with user=some_user %}
```

**Points clés :**

```
[OK] HTML sémantique
[OK] Logique d'affichage simple (if/for)
[OK] Templates réutilisables (include)
[OK] Pas de requêtes DB
[OK] Pas de calculs complexes
```

---

### [VIDEO_GAME] 3. LE CONTRÔLEUR (Controller)

**Rôle : Orchestrer les interactions entre Modèle et Vue**

**Responsabilités :**

```
[OK] Recevoir les requêtes HTTP
[OK] Valider les entrées utilisateur
[OK] Appeler les Modèles appropriés
[OK] Préparer les données pour la Vue
[OK] Retourner la réponse (HTML, JSON, redirect)
[X] PAS de logique métier complexe (-> Modèle)
[X] PAS de HTML (-> Vue)
```

**Exemple complet de Contrôleur :**

```python
# controllers/user_controller.py
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify, abort
from models.user import User
from models.post import Post
from decorators import login_required, admin_required
from utils.validators import validate_email, validate_password

# Créer le blueprint
users_bp = Blueprint('users', __name__, url_prefix='/users')

# ═════════════════════════════════════════════════════════════
# ROUTES DE LECTURE (GET)
# ═════════════════════════════════════════════════════════════

@users_bp.route('/')
def index():
    """
    Liste tous les utilisateurs
    GET /users/
    """
    # Pagination
    page = request.args.get('page', 1, type=int)
    per_page = 20
    
    # Recherche
    search_query = request.args.get('q', '')
    
    if search_query:
        users = User.search(search_query)
    else:
        users = User.get_active_users()
    
    # Paginer
    pagination = paginate(users, page, per_page)
    
    return render_template('users/index.html',
                         users=pagination.items,
                         pagination=pagination,
                         search_query=search_query)

@users_bp.route('/<int:user_id>')
def show(user_id):
    """
    Affiche un utilisateur
    GET /users/123
    """
    # Récupérer l'utilisateur
    user = User.query.get_or_404(user_id)
    
    # Vérifier si l'utilisateur est actif
    if not user.is_active and not current_user.is_admin:
        abort(403, "Ce compte est désactivé")
    
    # Statistiques
    followers_count = user.followers.count()
    following_count = user.following.count()
    
    # Vérifier si l'utilisateur courant suit cet utilisateur
    is_following = False
    if current_user.is_authenticated:
        is_following = current_user.is_following(user)
    
    # Afficher l'email seulement à l'utilisateur lui-même
    show_email = (current_user.is_authenticated and 
                  current_user.id == user.id)
    
    return render_template('users/show.html',
                         user=user,
                         followers_count=followers_count,
                         following_count=following_count,
                         is_following=is_following,
                         show_email=show_email)

@users_bp.route('/<int:user_id>/posts')
def posts(user_id):
    """
    Publications d'un utilisateur
    GET /users/123/posts
    """
    user = User.query.get_or_404(user_id)
    
    # Pagination
    page = request.args.get('page', 1, type=int)
    posts = Post.query.filter_by(author_id=user_id)\
                      .order_by(Post.created_at.desc())\
                      .paginate(page=page, per_page=10)
    
    return render_template('users/posts.html',
                         user=user,
                         posts=posts)

# ═════════════════════════════════════════════════════════════
# ROUTES DE CRÉATION (GET + POST)
# ═════════════════════════════════════════════════════════════

@users_bp.route('/new', methods=['GET', 'POST'])
def new():
    """
    Créer un utilisateur
    GET  /users/new  -> Affiche le formulaire
    POST /users/new  -> Crée l'utilisateur
    """
    if request.method == 'GET':
        # Afficher le formulaire
        return render_template('users/new.html')
    
    # POST : Traiter le formulaire
    username = request.form.get('username', '').strip()
    email = request.form.get('email', '').strip()
    password = request.form.get('password', '')
    password_confirm = request.form.get('password_confirm', '')
    first_name = request.form.get('first_name', '').strip()
    last_name = request.form.get('last_name', '').strip()
    birth_year = request.form.get('birth_year', type=int)
    
    # Validations
    errors = []
    
    if not username:
        errors.append("Le nom d'utilisateur est requis")
    elif len(username) < 3:
        errors.append("Le nom d'utilisateur doit faire au moins 3 caractères")
    elif User.find_by_username(username):
        errors.append("Ce nom d'utilisateur est déjà pris")
    
    if not email:
        errors.append("L'email est requis")
    elif not validate_email(email):
        errors.append("L'email n'est pas valide")
    elif User.find_by_email(email):
        errors.append("Cet email est déjà utilisé")
    
    if not password:
        errors.append("Le mot de passe est requis")
    elif not validate_password(password):
        errors.append("Le mot de passe est trop faible (min 8 caractères)")
    elif password != password_confirm:
        errors.append("Les mots de passe ne correspondent pas")
    
    if birth_year:
        age = datetime.now().year - birth_year
        if age < 13:
            errors.append("Vous devez avoir au moins 13 ans")
    
    # Si erreurs, réafficher le formulaire
    if errors:
        for error in errors:
            flash(error, 'error')
        return render_template('users/new.html',
                             username=username,
                             email=email,
                             first_name=first_name,
                             last_name=last_name,
                             birth_year=birth_year)
    
    # Créer l'utilisateur
    try:
        user = User.create(
            username=username,
            email=email,
            password=password,
            first_name=first_name,
            last_name=last_name,
            birth_year=birth_year
        )
        
        flash(f'Bienvenue {username} ! Votre compte a été créé.', 'success')
        
        # Connecter automatiquement
        login_user(user)
        
        return redirect(url_for('users.show', user_id=user.id))
        
    except Exception as e:
        flash('Erreur lors de la création du compte', 'error')
        app.logger.error(f"Erreur création utilisateur: {e}")
        return render_template('users/new.html',
                             username=username,
                             email=email,
                             first_name=first_name,
                             last_name=last_name,
                             birth_year=birth_year)

# ═════════════════════════════════════════════════════════════
# ROUTES DE MODIFICATION (GET + PUT/PATCH)
# ═════════════════════════════════════════════════════════════

@users_bp.route('/<int:user_id>/edit', methods=['GET', 'POST'])
@login_required
def edit(user_id):
    """
    Modifier un utilisateur
    GET  /users/123/edit  -> Affiche le formulaire
    POST /users/123/edit  -> Modifie l'utilisateur
    """
    user = User.query.get_or_404(user_id)
    
    # Vérifier les permissions
    if current_user.id != user.id and not current_user.is_admin:
        abort(403, "Vous n'avez pas la permission de modifier ce profil")
    
    if request.method == 'GET':
        # Afficher le formulaire pré-rempli
        return render_template('users/edit.html', user=user)
    
    # POST : Traiter les modifications
    first_name = request.form.get('first_name', '').strip()
    last_name = request.form.get('last_name', '').strip()
    email = request.form.get('email', '').strip()
    birth_year = request.form.get('birth_year', type=int)
    
    # Validations
    errors = []
    
    if email != user.email:
        if not validate_email(email):
            errors.append("Email invalide")
        elif User.find_by_email(email):
            errors.append("Cet email est déjà utilisé")
    
    if errors:
        for error in errors:
            flash(error, 'error')
        return render_template('users/edit.html', user=user)
    
    # Mettre à jour
    try:
        user.first_name = first_name
        user.last_name = last_name
        user.email = email
        user.birth_year = birth_year
        db.session.commit()
        
        flash('Profil mis à jour avec succès', 'success')
        return redirect(url_for('users.show', user_id=user.id))
        
    except Exception as e:
        db.session.rollback()
        flash('Erreur lors de la mise à jour', 'error')
        app.logger.error(f"Erreur update utilisateur: {e}")
        return render_template('users/edit.html', user=user)

# ═════════════════════════════════════════════════════════════
# ROUTES DE SUPPRESSION (DELETE)
# ═════════════════════════════════════════════════════════════

@users_bp.route('/<int:user_id>/delete', methods=['POST'])
@login_required
def delete(user_id):
    """
    Supprimer un utilisateur
    POST /users/123/delete
    """
    user = User.query.get_or_404(user_id)
    
    # Vérifier les permissions
    if current_user.id != user.id and not current_user.is_admin:
        abort(403)
    
    try:
        # Soft delete
        user.deactivate()
        
        flash('Compte désactivé avec succès', 'success')
        
        # Si l'utilisateur se supprime lui-même
        if current_user.id == user.id:
            logout_user()
            return redirect(url_for('home.index'))
        
        return redirect(url_for('users.index'))
        
    except Exception as e:
        flash('Erreur lors de la suppression', 'error')
        app.logger.error(f"Erreur delete utilisateur: {e}")
        return redirect(url_for('users.show', user_id=user.id))

# ═════════════════════════════════════════════════════════════
# ROUTES API (JSON)
# ═════════════════════════════════════════════════════════════

@users_bp.route('/api/users/<int:user_id>', methods=['GET'])
def api_get_user(user_id):
    """
    API : Récupérer un utilisateur
    GET /api/users/123
    """
    user = User.query.get_or_404(user_id)
    return jsonify(user.to_dict())

@users_bp.route('/api/users/search', methods=['GET'])
def api_search():
    """
    API : Rechercher des utilisateurs
    GET /api/users/search?q=john
    """
    query = request.args.get('q', '')
    
    if not query or len(query) < 2:
        return jsonify({'error': 'Query trop court'}), 400
    
    users = User.search(query)
    return jsonify([user.to_dict() for user in users])

# ═════════════════════════════════════════════════════════════
# ROUTES ACTIONS SPÉCIALES
# ═════════════════════════════════════════════════════════════

@users_bp.route('/<int:user_id>/follow', methods=['POST'])
@login_required
def follow(user_id):
    """
    Suivre un utilisateur
    POST /users/123/follow
    """
    user = User.query.get_or_404(user_id)
    
    if current_user.id == user.id:
        return jsonify({'error': 'Vous ne pouvez pas vous suivre vous-même'}), 400
    
    if current_user.is_following(user):
        return jsonify({'error': 'Vous suivez déjà cet utilisateur'}), 400
    
    current_user.follow(user)
    db.session.commit()
    
    return jsonify({
        'message': 'Utilisateur suivi',
        'followers_count': user.followers.count()
    })

@users_bp.route('/<int:user_id>/unfollow', methods=['POST'])
@login_required
def unfollow(user_id):
    """
    Ne plus suivre un utilisateur
    POST /users/123/unfollow
    """
    user = User.query.get_or_404(user_id)
    
    if not current_user.is_following(user):
        return jsonify({'error': 'Vous ne suivez pas cet utilisateur'}), 400
    
    current_user.unfollow(user)
    db.session.commit()
    
    return jsonify({
        'message': 'Utilisateur unfollow',
        'followers_count': user.followers.count()
    })
```

**Points clés :**

```
[OK] Gère UNIQUEMENT les requêtes HTTP
[OK] Valide les entrées
[OK] Délègue au Modèle
[OK] Prépare les données pour la Vue
[OK] Gère les erreurs et les messages
[OK] Code clair et organisé par responsabilité
```

---

## [SYNC] FLUX COMPLET MVC

### Exemple : "Créer un article"

**1. L'utilisateur remplit un formulaire**

```html
<!-- Vue : formulaire -->
<form action="/posts/new" method="POST">
    <input type="text" name="title" placeholder="Titre">
    <textarea name="content" placeholder="Contenu"></textarea>
    <button type="submit">Publier</button>
</form>
```

---

**2. Le navigateur envoie une requête POST**

```
POST /posts/new HTTP/1.1
Content-Type: application/x-www-form-urlencoded

title=Mon+super+article&content=Ceci+est+le+contenu
```

---

**3. Le Contrôleur reçoit la requête**

```python
@posts_bp.route('/new', methods=['POST'])
def create():
    # 3.1 Récupérer les données
    title = request.form.get('title')
    content = request.form.get('content')
    
    # 3.2 Valider
    if not title:
        flash('Le titre est requis', 'error')
        return redirect(url_for('posts.new'))
    
    # 3.3 Appeler le Modèle
    post = Post.create(
        title=title,
        content=content,
        author_id=current_user.id
    )
    
    # 3.4 Rediriger vers la Vue
    flash('Article publié !', 'success')
    return redirect(url_for('posts.show', post_id=post.id))
```

---

**4. Le Modèle sauvegarde en base**

```python
class Post(db.Model):
    @classmethod
    def create(cls, title, content, author_id):
        post = cls(
            title=title,
            content=content,
            author_id=author_id
        )
        db.session.add(post)
        db.session.commit()
        return post
```

---

**5. Le Contrôleur charge l'article créé**

```python
@posts_bp.route('/<int:post_id>')
def show(post_id):
    # Demander au Modèle
    post = Post.query.get_or_404(post_id)
    
    # Préparer pour la Vue
    return render_template('posts/show.html', post=post)
```

---

**6. La Vue affiche l'article**

```html
<!-- Vue : affichage -->
<article>
    <h1>{{ post.title }}</h1>
    <p>Par {{ post.author.username }}</p>
    <div>{{ post.content }}</div>
</article>
```

---

**7. Le HTML est renvoyé au navigateur**

```html
<article>
    <h1>Mon super article</h1>
    <p>Par john_doe</p>
    <div>Ceci est le contenu</div>
</article>
```

**[OK] Cycle complet ! [BRAVO]**

---

## [OUTILS] FRAMEWORKS MVC POPULAIRES

### [PYTHON] Python

**1. Django (Full-featured)**

```python
# models.py
from django.db import models

class User(models.Model):
    username = models.CharField(max_length=80)
    email = models.EmailField()

# views.py (Contrôleur dans Django)
from django.shortcuts import render
from .models import User

def show_user(request, user_id):
    user = User.objects.get(id=user_id)
    return render(request, 'users/show.html', {'user': user})

# templates/users/show.html (Vue)
<h1>{{ user.username }}</h1>
<p>{{ user.email }}</p>
```

---

**2. Flask (Léger et flexible)**

```python
# models.py
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80))

# controllers/user_controller.py
from flask import Blueprint, render_template
users_bp = Blueprint('users', __name__)

@users_bp.route('/users/<user_id>')
def show(user_id):
    user = User.query.get_or_404(user_id)
    return render_template('users/show.html', user=user)

# templates/users/show.html
<h1>{{ user.username }}</h1>
```

---

### [GEM_STONE] Ruby

**Ruby on Rails (Convention over Configuration)**

```ruby
# app/models/user.rb (Modèle)
class User < ApplicationRecord
  validates :username, presence: true, uniqueness: true
end

# app/controllers/users_controller.rb (Contrôleur)
class UsersController < ApplicationController
  def show
    @user = User.find(params[:id])
  end
end

# app/views/users/show.html.erb (Vue)
<h1><%= @user.username %></h1>
```

---

### [BLEU] PHP

**Laravel (Élégant)**

```php
// app/Models/User.php (Modèle)
class User extends Model
{
    protected $fillable = ['username', 'email'];
}

// app/Http/Controllers/UserController.php (Contrôleur)
class UserController extends Controller
{
    public function show($id)
    {
        $user = User::findOrFail($id);
        return view('users.show', compact('user'));
    }
}

// resources/views/users/show.blade.php (Vue)
<h1>{{ $user->username }}</h1>
```

---

### [VERT] JavaScript

**Express.js + EJS**

```javascript
// models/user.js (Modèle)
const User = {
    findById: (id) => {
        return db.query('SELECT * FROM users WHERE id = ?', [id]);
    }
};

// controllers/userController.js (Contrôleur)
exports.show = async (req, res) => {
    const user = await User.findById(req.params.id);
    res.render('users/show', { user });
};

// views/users/show.ejs (Vue)
<h1><%= user.username %></h1>
```

---

## [OBJECTIF] PROJET PRATIQUE : BLOG COMPLET MVC

Je vais créer un blog complet avec tous les fichiers organisés en MVC.

### Structure du projet

```
blog_mvc/
├─ app.py                  # Point d'entrée
├─ config.py               # Configuration
├─ requirements.txt        # Dépendances
│
├─ models/                 # MODÈLES
│   ├─ __init__.py
│   ├─ user.py
│   ├─ post.py
│   └─ comment.py
│
├─ controllers/            # CONTRÔLEURS
│   ├─ __init__.py
│   ├─ home_controller.py
│   ├─ user_controller.py
│   ├─ post_controller.py
│   └─ comment_controller.py
│
├─ views/                  # VUES
│   ├─ layout.html         # Template de base
│   ├─ home/
│   │   └─ index.html
│   ├─ users/
│   │   ├─ index.html
│   │   ├─ show.html
│   │   ├─ new.html
│   │   └─ edit.html
│   ├─ posts/
│   │   ├─ index.html
│   │   ├─ show.html
│   │   ├─ new.html
│   │   └─ edit.html
│   └─ partials/
│       ├─ header.html
│       ├─ footer.html
│       └─ flash_messages.html
│
├─ static/                 # Fichiers statiques
│   ├─ css/
│   │   └─ style.css
│   ├─ js/
│   │   └─ main.js
│   └─ img/
│
├─ migrations/             # Migrations DB
├─ tests/                  # Tests
└─ utils/                  # Utilitaires
    ├─ database.py
    ├─ validators.py
    └─ decorators.py
```

---

### Code complet

**app.py (Point d'entrée)**

```python
from flask import Flask, render_template
from config import config
from utils.database import db, init_db
from controllers.home_controller import home_bp
from controllers.user_controller import users_bp
from controllers.post_controller import posts_bp
from controllers.comment_controller import comments_bp
import os

def create_app(config_name=None):
    """Factory pour créer l'application"""
    
    if config_name is None:
        config_name = os.getenv('FLASK_ENV', 'development')
    
    app = Flask(__name__)
    app.config.from_object(config[config_name])
    
    # Initialiser la DB
    db.init_app(app)
    
    # Enregistrer les contrôleurs (blueprints)
    app.register_blueprint(home_bp)
    app.register_blueprint(users_bp)
    app.register_blueprint(posts_bp)
    app.register_blueprint(comments_bp)
    
    # Gestionnaire d'erreurs
    @app.errorhandler(404)
    def not_found(error):
        return render_template('errors/404.html'), 404
    
    @app.errorhandler(500)
    def server_error(error):
        return render_template('errors/500.html'), 500
    
    # Créer les tables
    with app.app_context():
        init_db()
    
    return app

if __name__ == '__main__':
    app = create_app()
    app.run(host='0.0.0.0', port=5000, debug=True)
```

---

**models/post.py (Modèle Article)**

```python
from datetime import datetime
from utils.database import db
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Boolean
from sqlalchemy.orm import relationship

class Post(db.Model):
    """Modèle Article"""
    
    __tablename__ = 'posts'
    
    # Colonnes
    id = Column(Integer, primary_key=True)
    title = Column(String(200), nullable=False)
    slug = Column(String(200), unique=True, nullable=False)
    content = Column(Text, nullable=False)
    excerpt = Column(String(500))
    author_id = Column(Integer, ForeignKey('users.id'), nullable=False)
    is_published = Column(Boolean, default=False)
    published_at = Column(DateTime)
    created_at = Column(DateTime, default=datetime.utcnow)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    views_count = Column(Integer, default=0)
    
    # Relations
    author = relationship('User', backref='posts')
    comments = relationship('Comment', backref='post', lazy=True, cascade='all, delete-orphan')
    
    # ─────────────────────────────────────────────────────────
    # MÉTHODES D'INSTANCE
    # ─────────────────────────────────────────────────────────
    
    def generate_slug(self):
        """Génère un slug depuis le titre"""
        import re
        slug = self.title.lower()
        slug = re.sub(r'[^a-z0-9\s-]', '', slug)
        slug = re.sub(r'[\s-]+', '-', slug)
        self.slug = slug[:200]
    
    def generate_excerpt(self, length=150):
        """Génère un extrait du contenu"""
        if len(self.content) <= length:
            self.excerpt = self.content
        else:
            self.excerpt = self.content[:length] + '...'
    
    def publish(self):
        """Publie l'article"""
        self.is_published = True
        self.published_at = datetime.utcnow()
        db.session.commit()
    
    def unpublish(self):
        """Dépublie l'article"""
        self.is_published = False
        self.published_at = None
        db.session.commit()
    
    def increment_views(self):
        """Incrémente le compteur de vues"""
        self.views_count += 1
        db.session.commit()
    
    def can_edit(self, user):
        """Vérifie si un utilisateur peut éditer l'article"""
        return user.is_authenticated and (user.id == self.author_id or user.is_admin)
    
    # ─────────────────────────────────────────────────────────
    # MÉTHODES DE CLASSE
    # ─────────────────────────────────────────────────────────
    
    @classmethod
    def create(cls, title, content, author_id, **kwargs):
        """Crée un nouvel article"""
        post = cls(title=title, content=content, author_id=author_id, **kwargs)
        post.generate_slug()
        post.generate_excerpt()
        db.session.add(post)
        db.session.commit()
        return post
    
    @classmethod
    def find_by_slug(cls, slug):
        """Trouve un article par son slug"""
        return cls.query.filter_by(slug=slug).first()
    
    @classmethod
    def get_published(cls):
        """Retourne tous les articles publiés"""
        return cls.query.filter_by(is_published=True)\
                       .order_by(cls.published_at.desc())\
                       .all()
    
    @classmethod
    def get_recent(cls, limit=5):
        """Retourne les articles récents"""
        return cls.query.filter_by(is_published=True)\
                       .order_by(cls.published_at.desc())\
                       .limit(limit)\
                       .all()
    
    @classmethod
    def search(cls, query):
        """Recherche des articles"""
        pattern = f"%{query}%"
        return cls.query.filter(
            cls.is_published == True,
            (cls.title.like(pattern)) | (cls.content.like(pattern))
        ).all()
    
    # ─────────────────────────────────────────────────────────
    # SÉRIALISATION
    # ─────────────────────────────────────────────────────────
    
    def to_dict(self):
        """Convertit en dictionnaire"""
        return {
            'id': self.id,
            'title': self.title,
            'slug': self.slug,
            'excerpt': self.excerpt,
            'author': self.author.to_dict() if self.author else None,
            'published_at': self.published_at.isoformat() if self.published_at else None,
            'views_count': self.views_count,
            'comments_count': len(self.comments)
        }
    
    def __repr__(self):
        return f'<Post {self.title}>'
```

---

**controllers/post_controller.py (Contrôleur Articles)**

```python
from flask import Blueprint, render_template, request, redirect, url_for, flash, abort
from models.post import Post
from models.comment import Comment
from utils.decorators import login_required
from utils.database import db

posts_bp = Blueprint('posts', __name__, url_prefix='/posts')

@posts_bp.route('/')
def index():
    """Liste des articles"""
    page = request.args.get('page', 1, type=int)
    posts = Post.get_published()
    
    return render_template('posts/index.html', posts=posts)

@posts_bp.route('/<slug>')
def show(slug):
    """Affiche un article"""
    post = Post.find_by_slug(slug)
    
    if not post:
        abort(404)
    
    # Incrémenter les vues
    post.increment_views()
    
    # Récupérer les commentaires
    comments = Comment.query.filter_by(post_id=post.id)\
                            .order_by(Comment.created_at.desc())\
                            .all()
    
    return render_template('posts/show.html', 
                         post=post, 
                         comments=comments)

@posts_bp.route('/new', methods=['GET', 'POST'])
@login_required
def new():
    """Créer un article"""
    if request.method == 'GET':
        return render_template('posts/new.html')
    
    # POST
    title = request.form.get('title', '').strip()
    content = request.form.get('content', '').strip()
    
    # Validation
    if not title:
        flash('Le titre est requis', 'error')
        return render_template('posts/new.html', title=title, content=content)
    
    if not content:
        flash('Le contenu est requis', 'error')
        return render_template('posts/new.html', title=title, content=content)
    
    # Créer l'article
    try:
        post = Post.create(
            title=title,
            content=content,
            author_id=current_user.id
        )
        
        flash('Article créé avec succès !', 'success')
        return redirect(url_for('posts.show', slug=post.slug))
        
    except Exception as e:
        flash('Erreur lors de la création', 'error')
        return render_template('posts/new.html', title=title, content=content)

@posts_bp.route('/<slug>/edit', methods=['GET', 'POST'])
@login_required
def edit(slug):
    """Modifier un article"""
    post = Post.find_by_slug(slug)
    
    if not post:
        abort(404)
    
    if not post.can_edit(current_user):
        abort(403)
    
    if request.method == 'GET':
        return render_template('posts/edit.html', post=post)
    
    # POST
    title = request.form.get('title', '').strip()
    content = request.form.get('content', '').strip()
    
    if not title or not content:
        flash('Titre et contenu requis', 'error')
        return render_template('posts/edit.html', post=post)
    
    try:
        post.title = title
        post.content = content
        post.generate_slug()
        post.generate_excerpt()
        db.session.commit()
        
        flash('Article mis à jour !', 'success')
        return redirect(url_for('posts.show', slug=post.slug))
        
    except Exception as e:
        db.session.rollback()
        flash('Erreur lors de la mise à jour', 'error')
        return render_template('posts/edit.html', post=post)

@posts_bp.route('/<slug>/delete', methods=['POST'])
@login_required
def delete(slug):
    """Supprimer un article"""
    post = Post.find_by_slug(slug)
    
    if not post:
        abort(404)
    
    if not post.can_edit(current_user):
        abort(403)
    
    try:
        db.session.delete(post)
        db.session.commit()
        
        flash('Article supprimé', 'success')
        return redirect(url_for('posts.index'))
        
    except Exception as e:
        flash('Erreur lors de la suppression', 'error')
        return redirect(url_for('posts.show', slug=post.slug))

@posts_bp.route('/<slug>/publish', methods=['POST'])
@login_required
def publish(slug):
    """Publier un article"""
    post = Post.find_by_slug(slug)
    
    if not post or not post.can_edit(current_user):
        abort(403)
    
    post.publish()
    flash('Article publié !', 'success')
    return redirect(url_for('posts.show', slug=post.slug))
```

---

**views/posts/show.html (Vue Article)**

```html
{% extends "layout.html" %}

{% block title %}{{ post.title }}{% endblock %}

{% block content %}
<div class="container">
    <article class="post">
        <!-- En-tête -->
        <header class="post-header">
            <h1 class="post-title">{{ post.title }}</h1>
            
            <div class="post-meta">
                <span class="post-author">
                    Par <a href="{{ url_for('users.show', user_id=post.author.id) }}">
                        {{ post.author.username }}
                    </a>
                </span>
                <span class="post-date">
                    {{ post.published_at|humanize }}
                </span>
                <span class="post-views">
                    {{ post.views_count }} vues
                </span>
            </div>
            
            {% if post.can_edit(current_user) %}
            <div class="post-actions">
                <a href="{{ url_for('posts.edit', slug=post.slug) }}" 
                   class="btn btn-sm btn-secondary">
                    Modifier
                </a>
                {% if not post.is_published %}
                <form action="{{ url_for('posts.publish', slug=post.slug) }}" 
                      method="POST" 
                      style="display:inline">
                    <button type="submit" class="btn btn-sm btn-success">
                        Publier
                    </button>
                </form>
                {% endif %}
                <form action="{{ url_for('posts.delete', slug=post.slug) }}" 
                      method="POST" 
                      style="display:inline"
                      onsubmit="return confirm('Êtes-vous sûr ?')">
                    <button type="submit" class="btn btn-sm btn-danger">
                        Supprimer
                    </button>
                </form>
            </div>
            {% endif %}
        </header>
        
        <!-- Contenu -->
        <div class="post-content">
            {{ post.content|safe }}
        </div>
    </article>
    
    <!-- Commentaires -->
    <section class="comments">
        <h2>Commentaires ({{ comments|length }})</h2>
        
        {% if current_user.is_authenticated %}
        <form action="{{ url_for('comments.create', post_slug=post.slug) }}" 
              method="POST" 
              class="comment-form">
            <textarea name="content" 
                      placeholder="Votre commentaire..." 
                      required></textarea>
            <button type="submit" class="btn btn-primary">
                Commenter
            </button>
        </form>
        {% else %}
        <p>
            <a href="{{ url_for('users.login') }}">Connectez-vous</a> 
            pour commenter
        </p>
        {% endif %}
        
        {% if comments %}
        <div class="comments-list">
            {% for comment in comments %}
            <div class="comment">
                <div class="comment-author">
                    <a href="{{ url_for('users.show', user_id=comment.author.id) }}">
                        {{ comment.author.username }}
                    </a>
                    <span class="comment-date">
                        {{ comment.created_at|humanize }}
                    </span>
                </div>
                <div class="comment-content">
                    {{ comment.content }}
                </div>
                {% if comment.can_delete(current_user) %}
                <form action="{{ url_for('comments.delete', comment_id=comment.id) }}" 
                      method="POST">
                    <button type="submit" class="btn btn-sm btn-link">
                        Supprimer
                    </button>
                </form>
                {% endif %}
            </div>
            {% endfor %}
        </div>
        {% endif %}
    </section>
</div>
{% endblock %}
```

**[OK] Application MVC complète ! [BRAVO]**

---

## [OBJECTIF] EXERCICES PRATIQUES

### Exercice 1 : Todo List MVC **

**Créer une application Todo List complète en MVC :**

**Fonctionnalités :**
- Créer une tâche
- Lister les tâches
- Marquer comme terminée
- Supprimer une tâche
- Filtrer (toutes/actives/terminées)

**Structure :**
```
models/task.py
controllers/task_controller.py
views/tasks/index.html
```

---

### Exercice 2 : Système de votes ***

**Ajouter un système de votes aux articles du blog :**

**Fonctionnalités :**
- Voter pour un article (upvote/downvote)
- Compter les votes
- Afficher le score
- Empêcher le double vote

---

### Exercice 3 : Catégories et tags ****

**Ajouter catégories et tags aux articles :**

**Fonctionnalités :**
- Un article appartient à une catégorie
- Un article peut avoir plusieurs tags
- Filtrer par catégorie
- Filtrer par tag
- Page de catégorie
- Page de tag

---

## [BRAVO] CONCLUSION

### Ce que tu as appris

**Concepts MVC :**
- [OK] Séparation Modèle-Vue-Contrôleur
- [OK] Flux de données MVC
- [OK] Responsabilités de chaque composant
- [OK] Avantages et inconvénients

**Pratique :**
- [OK] Structure complète d'une app MVC
- [OK] Code de Modèles, Vues, Contrôleurs
- [OK] Projet de blog complet
- [OK] Frameworks populaires

---

### Points clés

```
[OBJECTIF] MVC = Séparation des responsabilités
[OBJECTIF] Modèle = Données + Logique métier
[OBJECTIF] Vue = Présentation
[OBJECTIF] Contrôleur = Chef d'orchestre
[OBJECTIF] Facilite maintenance et collaboration
[OBJECTIF] Architecture standard du web
```

---

### Prochaines étapes

1. Code le projet Todo List
2. Ajoute des features au blog
3. Lis `architecture_layered.txt`
4. Compare MVC vs Layered Architecture

**[RAPIDE] Bravo ! Tu maîtrises maintenant MVC ! [RAPIDE]**

═══════════════════════════════════════════════════════════════
FIN DU FICHIER : architecture_mvc.txt
═══════════════════════════════════════════════════════════════

# [CLASSICAL_BUILDING] CLEAN ARCHITECTURE - GUIDE ULTRA DÉTAILLÉ

## * INTRODUCTION

### Qu'est-ce que la Clean Architecture ?

**Clean Architecture** est un **pattern architectural** créé par **Robert C. Martin (Uncle Bob)** qui met l'accent sur :

```
[OBJECTIF] L'INDÉPENDANCE
   ├─ Indépendance des frameworks
   ├─ Indépendance de la base de données
   ├─ Indépendance de l'UI
   └─ Indépendance des services externes

[TEST] LA TESTABILITÉ
   └─ Tout peut être testé sans dépendances externes

[PACKAGE] LA MAINTENABILITÉ
   └─ Code facile à modifier et à faire évoluer
```

**Analogie : Maison modulaire [ACCUEIL]**

```
[ACCUEIL] MAISON TRADITIONNELLE (code couplé)
   ├─ Murs porteurs <- Impossible de déplacer sans tout casser
   ├─ Tuyauterie fixe <- Changer = gros travaux
   └─ Électricité intégrée <- Modifier = danger

[CONSTRUCTION] MAISON MODULAIRE (Clean Architecture)
   ├─ Modules indépendants <- On peut changer un module
   ├─ Connexions standardisées <- Brancher/débrancher facilement
   └─ Remplacement facile <- Nouveau module = même interface
```

**En code :**

```
[X] Architecture traditionnelle :
Application -> Framework -> Base de données
   (Tout est collé ensemble)

[OK] Clean Architecture :
Logique métier (centre)
   ^
Interfaces (contrats)
   ^
Implémentations (interchangeables)
   (La logique ne connaît PAS les détails)
```

---

### [GRAPHIQUE] Le cercle de Clean Architecture

**Diagramme conceptuel :**

```
┌─────────────────────────────────────────────────────────┐
│  [WEB] FRAMEWORKS & DRIVERS (Cercle externe)              │
│  ├─ Web                                                 │
│  ├─ Base de données                                     │
│  ├─ Devices                                             │
│  └─ Services externes                                   │
│                                                         │
│  ┌───────────────────────────────────────────────────┐ │
│  │  [VIDEO_GAME] INTERFACE ADAPTERS (Adaptateurs)             │ │
│  │  ├─ Controllers                                   │ │
│  │  ├─ Gateways                                      │ │
│  │  ├─ Presenters                                    │ │
│  │  └─ Repositories                                  │ │
│  │                                                   │ │
│  │  ┌─────────────────────────────────────────────┐ │ │
│  │  │  [PRO] APPLICATION BUSINESS RULES             │ │ │
│  │  │  ├─ Use Cases (Cas d'usage)                │ │ │
│  │  │  ├─ Input Ports                            │ │ │
│  │  │  └─ Output Ports                           │ │ │
│  │  │                                            │ │ │
│  │  │  ┌─────────────────────────────────────┐  │ │ │
│  │  │  │  [ENTREPRISE] ENTERPRISE BUSINESS RULES      │  │ │ │
│  │  │  │  ├─ Entities (Entités)             │  │ │ │
│  │  │  │  └─ Domain Models                   │  │ │ │
│  │  │  └─────────────────────────────────────┘  │ │ │
│  │  └─────────────────────────────────────────────┘ │ │
│  └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘

RÈGLE D'OR : Les dépendances pointent TOUJOURS vers l'intérieur
           (Le centre ne connaît RIEN de l'extérieur)
```

---

### [OBJECTIF] Les 4 couches en détail

**1. ENTITIES (Entités) - Cercle le plus interne**

```
[ENTREPRISE] Rôle : Règles métier de l'entreprise
[PACKAGE] Contenu : 
   - Objets métier purs
   - Logique business invariante
   - Pas de dépendances externes

Exemple :
class Order:
    def calculate_total(self):
        # Logique métier pure
        return sum(item.price for item in self.items)
```

---

**2. USE CASES (Cas d'usage) - Application Business Rules**

```
[PRO] Rôle : Orchestration de la logique applicative
[PACKAGE] Contenu :
   - Scénarios d'utilisation de l'app
   - Coordination des entités
   - Input/Output ports (interfaces)

Exemple :
class CreateOrderUseCase:
    def execute(self, order_data):
        # 1. Créer l'entité
        order = Order.create(order_data)
        # 2. Valider
        order.validate()
        # 3. Sauvegarder (via port)
        self.repository.save(order)
        return order
```

---

**3. INTERFACE ADAPTERS (Adaptateurs)**

```
[VIDEO_GAME] Rôle : Convertir les données entre use cases et monde extérieur
[PACKAGE] Contenu :
   - Controllers (Web, CLI, etc.)
   - Presenters (formatage de sortie)
   - Repositories (implémentations)
   - Gateways (services externes)

Exemple :
class OrderController:
    def create_order(self, request):
        # Convertir HTTP -> Use Case
        order_data = self.parse_request(request)
        # Appeler le use case
        order = self.create_order_use_case.execute(order_data)
        # Convertir Use Case -> HTTP
        return self.format_response(order)
```

---

**4. FRAMEWORKS & DRIVERS (Détails techniques)**

```
[WEB] Rôle : Outils et frameworks concrets
[PACKAGE] Contenu :
   - Framework web (Flask, Django, FastAPI)
   - Base de données (PostgreSQL, MongoDB)
   - Services externes (Stripe, SendGrid)
   - Devices (IoT, capteurs)

Exemple :
# PostgreSQL (remplaçable par MySQL sans toucher au reste)
class PostgreSQLOrderRepository(OrderRepository):
    def save(self, order):
        # Implémentation spécifique PostgreSQL
        pass
```

---

## [REFLEXION] POURQUOI UTILISER CLEAN ARCHITECTURE ?

### [OK] Avantages

**1. Indépendance des frameworks [PLUGIN]**

**Problème traditionnel :**

```python
# [X] Code couplé à Django
from django.db import models
from django.contrib.auth.models import User

class Order(models.Model):
    """Entité complètement couplée à Django"""
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    total = models.DecimalField(max_digits=10, decimal_places=2)
    
    def calculate_shipping(self):
        # Logique métier mélangée avec Django
        pass

# Si tu veux changer de framework -> TOUT à réécrire ! [!]
```

---

**Avec Clean Architecture :**

```python
# [OK] Entité pure (indépendante)
class Order:
    """Entité business pure - Aucune dépendance externe"""
    def __init__(self, user_id, items):
        self.user_id = user_id
        self.items = items
    
    def calculate_total(self):
        """Logique métier pure"""
        return sum(item.price * item.quantity for item in self.items)
    
    def calculate_shipping(self):
        """Logique métier pure"""
        if self.calculate_total() > 100:
            return 0  # Livraison gratuite
        return 10

# Infrastructure séparée
class DjangoOrderRepository(OrderRepository):
    """Adaptateur Django - Remplaçable"""
    def save(self, order):
        DjangoOrderModel.objects.create(
            user_id=order.user_id,
            total=order.calculate_total()
        )

class FlaskOrderRepository(OrderRepository):
    """Adaptateur Flask - Alternative"""
    def save(self, order):
        # Implémentation Flask/SQLAlchemy
        pass
```

**Bénéfice :**
- Changer de Django -> Flask = Changer uniquement le repository
- La logique métier reste intacte [OK]
- Tests sans Django [OK]

---

**2. Indépendance de la base de données [ARCHIVE]**

**Problème traditionnel :**

```python
# [X] Code couplé à SQL
def get_user_orders(user_id):
    cursor = db.execute("""
        SELECT o.*, u.name 
        FROM orders o
        JOIN users u ON o.user_id = u.id
        WHERE u.id = ?
    """, (user_id,))
    
    return cursor.fetchall()

# Changer de SQL -> MongoDB = TOUT réécrire
```

---

**Avec Clean Architecture :**

```python
# [OK] Interface (contrat)
class OrderRepository(ABC):
    @abstractmethod
    def find_by_user(self, user_id):
        pass

# Implémentation SQL
class SQLOrderRepository(OrderRepository):
    def find_by_user(self, user_id):
        cursor = db.execute("SELECT * FROM orders WHERE user_id = ?", user_id)
        return [Order.from_db(row) for row in cursor.fetchall()]

# Implémentation MongoDB (alternative)
class MongoOrderRepository(OrderRepository):
    def find_by_user(self, user_id):
        docs = mongo.orders.find({"user_id": user_id})
        return [Order.from_mongo(doc) for doc in docs]

# Use Case (ne connaît PAS le type de DB)
class GetUserOrdersUseCase:
    def __init__(self, order_repository: OrderRepository):
        self.order_repository = order_repository
    
    def execute(self, user_id):
        return self.order_repository.find_by_user(user_id)

# Changer de DB = Changer l'injection de dépendance
# SQL
use_case = GetUserOrdersUseCase(SQLOrderRepository())

# MongoDB (juste changer une ligne !)
use_case = GetUserOrdersUseCase(MongoOrderRepository())
```

**Bénéfice :**
- Migration de DB sans toucher la logique [OK]
- Tests avec DB en mémoire [OK]
- Plusieurs DB en parallèle possible [OK]

---

**3. Testabilité maximale [TEST]**

**Avec Clean Architecture, tu peux tester TOUT sans dépendances :**

```python
# Test de l'ENTITÉ (logique métier pure)
def test_order_total():
    order = Order(user_id=1, items=[
        Item(price=10, quantity=2),
        Item(price=5, quantity=3)
    ])
    
    assert order.calculate_total() == 35
    # Aucune DB, aucun framework, aucun réseau [OK]

# Test du USE CASE (avec mock)
def test_create_order_use_case():
    # Mock du repository
    mock_repo = Mock(spec=OrderRepository)
    
    # Use case avec mock
    use_case = CreateOrderUseCase(mock_repo)
    
    # Exécution
    order = use_case.execute(order_data)
    
    # Vérifications
    assert order.calculate_total() == 35
    mock_repo.save.assert_called_once()
    # Test rapide, sans vraie DB [OK]

# Test de l'ADAPTATEUR (intégration)
def test_sql_repository():
    # DB de test en mémoire
    repo = SQLOrderRepository(test_db)
    
    order = Order(user_id=1, items=[...])
    repo.save(order)
    
    saved_order = repo.find_by_id(order.id)
    assert saved_order.calculate_total() == order.calculate_total()
    # Test d'intégration isolé [OK]
```

**Pyramide de tests :**

```
        ^ Peu de tests
       /  \
      / UI \      <- Tests E2E (lents, peu nombreux)
     /──────\
    /        \
   / Adapters \   <- Tests d'intégration (moyens)
  /────────────\
 /              \
/   Use Cases    \ <- Tests unitaires (rapides, nombreux)
──────────────────
/    Entities     \ <- Tests unitaires (très rapides, très nombreux)
────────────────────
```

---

**4. Indépendance de l'UI [DESIGN]**

```python
# [OK] Use case agnostique de l'UI
class GetProductsUseCase:
    def execute(self, filters):
        products = self.product_repository.find(filters)
        return products

# UI Web
@app.route('/products')
def web_products():
    products = use_case.execute(request.args)
    return render_template('products.html', products=products)

# UI CLI
def cli_products(args):
    products = use_case.execute(args)
    for product in products:
        print(f"{product.name}: ${product.price}")

# UI API REST
@api.route('/api/products')
def api_products():
    products = use_case.execute(request.args)
    return jsonify([p.to_dict() for p in products])

# UI GraphQL
@graphql.field('products')
def graphql_products(filters):
    products = use_case.execute(filters)
    return products
```

**Même use case, 4 UIs différentes ! [OK]**

---

**5. Changements localisés [OBJECTIF]**

**Scénario : Ajouter un système de cache**

**Architecture traditionnelle :**

```python
# [X] Modifier PARTOUT où on récupère des données
def get_user(user_id):
    # Ajouter du cache ici
    cached = cache.get(f'user:{user_id}')
    if cached:
        return cached
    user = db.query(...)
    cache.set(f'user:{user_id}', user)
    return user

# Répéter pour 50+ fonctions... [!]
```

---

**Clean Architecture :**

```python
# [OK] Ajouter un adaptateur "décorateur"
class CachedUserRepository(UserRepository):
    def __init__(self, base_repository, cache):
        self.base_repository = base_repository
        self.cache = cache
    
    def find_by_id(self, user_id):
        cached = self.cache.get(f'user:{user_id}')
        if cached:
            return cached
        
        user = self.base_repository.find_by_id(user_id)
        self.cache.set(f'user:{user_id}', user)
        return user

# Injection (UNE SEULE ligne à changer)
base_repo = SQLUserRepository()
cached_repo = CachedUserRepository(base_repo, redis_cache)

use_case = GetUserUseCase(cached_repo)  # <- Tout le reste fonctionne !
```

**Modification localisée = Moins de bugs ! [OK]**

---

### [X] Inconvénients

**1. Complexité initiale [DOCS]**

**Pour un projet simple :**

```
Simple script Python (10 lignes) :
def hello():
    print("Hello World")

Même chose en Clean Architecture (50+ fichiers) :
entities/
use_cases/
adapters/
    controllers/
    repositories/
    presenters/
frameworks/
    web/
    database/
config/
tests/

[!] Overkill pour un "Hello World"
```

---

**2. Plus de code (boilerplate) [NOTE]**

**Exemple : Créer un utilisateur**

**Code traditionnel :**

```python
# [X] Tout dans 1 fonction (15 lignes)
@app.route('/users', methods=['POST'])
def create_user():
    username = request.json['username']
    email = request.json['email']
    
    if User.query.filter_by(email=email).first():
        return {'error': 'Email exists'}, 400
    
    user = User(username=username, email=email)
    db.session.add(user)
    db.session.commit()
    
    return {'id': user.id}, 201
```

---

**Clean Architecture :**

```python
# [OK] Séparé en plusieurs fichiers (100+ lignes)

# entities/user.py
class User:
    def __init__(self, username, email):
        self.username = username
        self.email = email
    
    def validate(self):
        if not self.email or '@' not in self.email:
            raise ValueError("Invalid email")

# use_cases/create_user.py
class CreateUserUseCase:
    def __init__(self, user_repository):
        self.user_repository = user_repository
    
    def execute(self, username, email):
        # Vérifier existence
        if self.user_repository.exists_by_email(email):
            raise DuplicateEmailError()
        
        # Créer entité
        user = User(username, email)
        user.validate()
        
        # Sauvegarder
        return self.user_repository.save(user)

# adapters/repositories/sql_user_repository.py
class SQLUserRepository(UserRepository):
    def exists_by_email(self, email):
        return bool(self.db.query(UserModel).filter_by(email=email).first())
    
    def save(self, user):
        model = UserModel(username=user.username, email=user.email)
        self.db.session.add(model)
        self.db.session.commit()
        return user

# adapters/controllers/user_controller.py
class UserController:
    def __init__(self, create_user_use_case):
        self.create_user_use_case = create_user_use_case
    
    def create(self, request):
        try:
            username = request.json['username']
            email = request.json['email']
            
            user = self.create_user_use_case.execute(username, email)
            
            return {'id': user.id}, 201
        except DuplicateEmailError:
            return {'error': 'Email exists'}, 400
        except ValueError as e:
            return {'error': str(e)}, 400
```

**Plus de code, MAIS :**
- [OK] Chaque partie est testable
- [OK] Chaque partie est réutilisable
- [OK] Chaque partie est indépendante

---

**3. Courbe d'apprentissage raide [ALARM_CLOCK]**

**Concepts à maîtriser :**

```
Débutant :
├─ Séparation en couches
├─ Dependency Inversion
├─ Injection de dépendances
├─ Interfaces/Contrats
└─ Use Cases

Intermédiaire :
├─ Ports & Adapters
├─ Repository Pattern
├─ Gateway Pattern
├─ Presenter Pattern
└─ DTO (Data Transfer Objects)

Avancé :
├─ Command/Query Separation (CQRS)
├─ Event Sourcing
├─ Domain Events
└─ Aggregate Roots

Temps d'apprentissage : 4-8 semaines
```

---

**4. Over-engineering possible [CONSTRUCTION]**

**Exemple de mauvaise utilisation :**

```python
# [X] Clean Architecture pour un script one-shot
# fetch_data.py (devrait être 10 lignes)

# entities/data.py
class Data:
    pass

# use_cases/fetch_data.py
class FetchDataUseCase:
    pass

# adapters/repositories/data_repository.py
class DataRepository:
    pass

# 20 fichiers pour un script simple... [!]

# [OK] Solution : Pour les scripts simples, reste simple !
def fetch_data():
    return requests.get('https://api.example.com/data').json()
```

**Règle d'or :**

```
Projet < 1 mois -> Pas de Clean Architecture
Projet 1-6 mois -> Clean Architecture optionnelle
Projet > 6 mois -> Clean Architecture recommandée
Projet > 1 an -> Clean Architecture fortement recommandée
```

---

## [CONSTRUCTION] STRUCTURE COMPLÈTE D'UN PROJET

### [DOSSIER] Organisation des dossiers

```
clean_ecommerce/
│
├─ domain/                         # ENTITIES (Logique métier pure)
│   ├─ __init__.py
│   ├─ entities/
│   │   ├─ __init__.py
│   │   ├─ user.py                # Entité User
│   │   ├─ product.py             # Entité Product
│   │   ├─ order.py               # Entité Order
│   │   └─ cart.py                # Entité Cart
│   │
│   ├─ value_objects/              # Value Objects
│   │   ├─ __init__.py
│   │   ├─ email.py               # Email (validation)
│   │   ├─ money.py               # Money (montant + devise)
│   │   └─ address.py             # Address
│   │
│   └─ exceptions/                 # Exceptions métier
│       ├─ __init__.py
│       ├─ user_exceptions.py
│       └─ order_exceptions.py
│
├─ application/                    # USE CASES (Logique applicative)
│   ├─ __init__.py
│   ├─ use_cases/
│   │   ├─ __init__.py
│   │   ├─ user/
│   │   │   ├─ create_user.py
│   │   │   ├─ authenticate_user.py
│   │   │   └─ update_profile.py
│   │   ├─ product/
│   │   │   ├─ list_products.py
│   │   │   ├─ get_product.py
│   │   │   └─ search_products.py
│   │   └─ order/
│   │       ├─ create_order.py
│   │       ├─ cancel_order.py
│   │       └─ get_order_history.py
│   │
│   ├─ interfaces/                 # Interfaces (contrats)
│   │   ├─ __init__.py
│   │   ├─ repositories/          # Interfaces repositories
│   │   │   ├─ user_repository.py
│   │   │   ├─ product_repository.py
│   │   │   └─ order_repository.py
│   │   └─ gateways/              # Interfaces gateways
│   │       ├─ email_gateway.py
│   │       ├─ payment_gateway.py
│   │       └─ notification_gateway.py
│   │
│   └─ dtos/                       # Data Transfer Objects
│       ├─ __init__.py
│       ├─ user_dto.py
│       ├─ product_dto.py
│       └─ order_dto.py
│
├─ infrastructure/                 # ADAPTERS (Implémentations)
│   ├─ __init__.py
│   ├─ repositories/               # Implémentations repositories
│   │   ├─ __init__.py
│   │   ├─ sql_user_repository.py
│   │   ├─ sql_product_repository.py
│   │   └─ sql_order_repository.py
│   │
│   ├─ gateways/                   # Implémentations gateways
│   │   ├─ __init__.py
│   │   ├─ smtp_email_gateway.py
│   │   ├─ stripe_payment_gateway.py
│   │   └─ fcm_notification_gateway.py
│   │
│   ├─ database/                   # Configuration DB
│   │   ├─ __init__.py
│   │   ├─ connection.py
│   │   ├─ models.py              # ORM models (SQLAlchemy)
│   │   └─ migrations/
│   │
│   └─ cache/                      # Système de cache
│       ├─ __init__.py
│       └─ redis_cache.py
│
├─ presentation/                   # FRAMEWORKS (Web, API, CLI)
│   ├─ __init__.py
│   ├─ web/                        # Interface Web
│   │   ├─ __init__.py
│   │   ├─ app.py                 # Application Flask/FastAPI
│   │   ├─ controllers/
│   │   │   ├─ __init__.py
│   │   │   ├─ user_controller.py
│   │   │   ├─ product_controller.py
│   │   │   └─ order_controller.py
│   │   ├─ middlewares/
│   │   │   ├─ auth_middleware.py
│   │   │   └─ error_handler.py
│   │   └─ templates/             # Templates HTML
│   │       └─ ...
│   │
│   ├─ api/                        # API REST
│   │   ├─ __init__.py
│   │   ├─ app.py
│   │   ├─ routes/
│   │   │   ├─ user_routes.py
│   │   │   ├─ product_routes.py
│   │   │   └─ order_routes.py
│   │   └─ serializers/
│   │       └─ ...
│   │
│   └─ cli/                        # Interface CLI
│       ├─ __init__.py
│       └─ commands/
│           ├─ seed_database.py
│           └─ generate_report.py
│
├─ tests/                          # Tests
│   ├─ unit/                       # Tests unitaires
│   │   ├─ domain/
│   │   │   ├─ test_user_entity.py
│   │   │   └─ test_order_entity.py
│   │   └─ application/
│   │       └─ test_create_order.py
│   │
│   ├─ integration/                # Tests d'intégration
│   │   └─ infrastructure/
│   │       └─ test_sql_repositories.py
│   │
│   └─ e2e/                        # Tests end-to-end
│       └─ test_order_workflow.py
│
├─ config/                         # Configuration
│   ├─ __init__.py
│   ├─ settings.py
│   ├─ dependency_injection.py    # Container DI
│   └─ logging.py
│
├─ main.py                         # Point d'entrée
├─ requirements.txt
├─ README.md
└─ .env
```

---

## [CODE] CODE COMPLET - EXEMPLE E-COMMERCE

### 1. DOMAIN (Entités)

**domain/entities/user.py**

```python
from datetime import datetime
from domain.value_objects.email import Email
from domain.exceptions.user_exceptions import InvalidCredentialsError

class User:
    """
    Entité User - Logique métier pure
    Aucune dépendance externe
    """
    
    def __init__(
        self,
        user_id: int = None,
        username: str = None,
        email: Email = None,
        password_hash: str = None,
        created_at: datetime = None
    ):
        self.id = user_id
        self.username = username
        self.email = email
        self.password_hash = password_hash
        self.created_at = created_at or datetime.now()
        self.is_active = True
    
    def validate(self):
        """Valide l'entité"""
        errors = []
        
        if not self.username or len(self.username) < 3:
            errors.append("Username must be at least 3 characters")
        
        if not self.email:
            errors.append("Email is required")
        elif not self.email.is_valid():
            errors.append("Email is invalid")
        
        if errors:
            raise ValueError(", ".join(errors))
    
    def deactivate(self):
        """Désactive le compte"""
        self.is_active = False
    
    def activate(self):
        """Active le compte"""
        self.is_active = True
    
    def change_username(self, new_username: str):
        """Change le username avec validation"""
        if len(new_username) < 3:
            raise ValueError("Username too short")
        self.username = new_username
    
    @staticmethod
    def check_password(plain_password: str, hashed_password: str) -> bool:
        """Vérifie le mot de passe"""
        import bcrypt
        return bcrypt.checkpw(
            plain_password.encode('utf-8'),
            hashed_password.encode('utf-8')
        )
    
    @staticmethod
    def hash_password(plain_password: str) -> str:
        """Hash le mot de passe"""
        import bcrypt
        return bcrypt.hashpw(
            plain_password.encode('utf-8'),
            bcrypt.gensalt()
        ).decode('utf-8')
    
    def __repr__(self):
        return f'<User {self.username}>'
```

**domain/entities/product.py**

```python
from domain.value_objects.money import Money
from domain.exceptions.product_exceptions import InsufficientStockError

class Product:
    """Entité Product - Logique métier pure"""
    
    def __init__(
        self,
        product_id: int = None,
        name: str = None,
        description: str = None,
        price: Money = None,
        stock: int = 0
    ):
        self.id = product_id
        self.name = name
        self.description = description
        self.price = price
        self.stock = stock
        self.is_active = True
    
    def validate(self):
        """Valide le produit"""
        errors = []
        
        if not self.name:
            errors.append("Product name is required")
        
        if not self.price or self.price.amount <= 0:
            errors.append("Price must be positive")
        
        if self.stock < 0:
            errors.append("Stock cannot be negative")
        
        if errors:
            raise ValueError(", ".join(errors))
    
    def is_available(self) -> bool:
        """Vérifie si le produit est disponible"""
        return self.is_active and self.stock > 0
    
    def reduce_stock(self, quantity: int):
        """Réduit le stock"""
        if quantity > self.stock:
            raise InsufficientStockError(
                f"Insufficient stock. Available: {self.stock}, Requested: {quantity}"
            )
        self.stock -= quantity
    
    def increase_stock(self, quantity: int):
        """Augmente le stock"""
        if quantity < 0:
            raise ValueError("Quantity must be positive")
        self.stock += quantity
    
    def update_price(self, new_price: Money):
        """Met à jour le prix"""
        if new_price.amount <= 0:
            raise ValueError("Price must be positive")
        self.price = new_price
    
    def __repr__(self):
        return f'<Product {self.name}>'
```

**domain/entities/order.py**

```python
from datetime import datetime
from typing import List
from domain.value_objects.money import Money
from domain.exceptions.order_exceptions import OrderAlreadyPaidError

class OrderItem:
    """Item de commande"""
    def __init__(self, product_id: int, product_name: str, 
                 price: Money, quantity: int):
        self.product_id = product_id
        self.product_name = product_name
        self.price = price
        self.quantity = quantity
    
    def subtotal(self) -> Money:
        """Calcule le sous-total"""
        return Money(
            self.price.amount * self.quantity,
            self.price.currency
        )

class Order:
    """Entité Order - Logique métier pure"""
    
    STATUS_PENDING = 'pending'
    STATUS_PAID = 'paid'
    STATUS_SHIPPED = 'shipped'
    STATUS_DELIVERED = 'delivered'
    STATUS_CANCELLED = 'cancelled'
    
    def __init__(
        self,
        order_id: int = None,
        user_id: int = None,
        items: List[OrderItem] = None,
        status: str = STATUS_PENDING,
        created_at: datetime = None
    ):
        self.id = order_id
        self.user_id = user_id
        self.items = items or []
        self.status = status
        self.created_at = created_at or datetime.now()
        self.paid_at = None
        self.shipped_at = None
    
    def validate(self):
        """Valide la commande"""
        if not self.items:
            raise ValueError("Order must have at least one item")
        
        if not self.user_id:
            raise ValueError("Order must have a user")
    
    def calculate_total(self) -> Money:
        """Calcule le total de la commande"""
        if not self.items:
            return Money(0, 'USD')
        
        total = sum(item.subtotal().amount for item in self.items)
        currency = self.items[0].price.currency
        
        return Money(total, currency)
    
    def calculate_shipping(self) -> Money:
        """Calcule les frais de livraison"""
        total = self.calculate_total()
        
        # Livraison gratuite au-dessus de 100
        if total.amount >= 100:
            return Money(0, total.currency)
        
        return Money(10, total.currency)
    
    def calculate_grand_total(self) -> Money:
        """Calcule le total avec livraison"""
        total = self.calculate_total()
        shipping = self.calculate_shipping()
        
        return Money(
            total.amount + shipping.amount,
            total.currency
        )
    
    def mark_as_paid(self):
        """Marque la commande comme payée"""
        if self.status == self.STATUS_PAID:
            raise OrderAlreadyPaidError("Order is already paid")
        
        self.status = self.STATUS_PAID
        self.paid_at = datetime.now()
    
    def ship(self):
        """Expédie la commande"""
        if self.status != self.STATUS_PAID:
            raise ValueError("Cannot ship unpaid order")
        
        self.status = self.STATUS_SHIPPED
        self.shipped_at = datetime.now()
    
    def deliver(self):
        """Livre la commande"""
        if self.status != self.STATUS_SHIPPED:
            raise ValueError("Cannot deliver unshipped order")
        
        self.status = self.STATUS_DELIVERED
    
    def cancel(self):
        """Annule la commande"""
        if self.status in [self.STATUS_SHIPPED, self.STATUS_DELIVERED]:
            raise ValueError("Cannot cancel shipped/delivered order")
        
        self.status = self.STATUS_CANCELLED
    
    def __repr__(self):
        return f'<Order {self.id} - {self.status}>'
```

**domain/value_objects/money.py**

```python
class Money:
    """Value Object pour représenter de l'argent"""
    
    def __init__(self, amount: float, currency: str = 'USD'):
        if amount < 0:
            raise ValueError("Amount cannot be negative")
        
        self.amount = round(amount, 2)  # 2 décimales
        self.currency = currency.upper()
    
    def __eq__(self, other):
        if not isinstance(other, Money):
            return False
        return (self.amount == other.amount and 
                self.currency == other.currency)
    
    def __add__(self, other):
        if self.currency != other.currency:
            raise ValueError("Cannot add different currencies")
        return Money(self.amount + other.amount, self.currency)
    
    def __repr__(self):
        return f'{self.amount} {self.currency}'
```

**domain/value_objects/email.py**

```python
import re

class Email:
    """Value Object pour email avec validation"""
    
    EMAIL_REGEX = re.compile(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
    
    def __init__(self, email: str):
        self.value = email.lower().strip()
        if not self.is_valid():
            raise ValueError(f"Invalid email: {email}")
    
    def is_valid(self) -> bool:
        """Valide l'email"""
        return bool(self.EMAIL_REGEX.match(self.value))
    
    def __eq__(self, other):
        if not isinstance(other, Email):
            return False
        return self.value == other.value
    
    def __repr__(self):
        return self.value
```

---

### 2. APPLICATION (Use Cases)

**application/use_cases/user/create_user.py**

```python
from domain.entities.user import User
from domain.value_objects.email import Email
from application.interfaces.repositories.user_repository import UserRepository
from application.interfaces.gateways.email_gateway import EmailGateway
from application.dtos.user_dto import UserDTO

class CreateUserUseCase:
    """
    Use Case : Créer un utilisateur
    Orchestration de la logique applicative
    """
    
    def __init__(
        self,
        user_repository: UserRepository,
        email_gateway: EmailGateway
    ):
        self.user_repository = user_repository
        self.email_gateway = email_gateway
    
    def execute(self, username: str, email_str: str, password: str) -> UserDTO:
        """
        Exécute le use case
        
        Args:
            username: Nom d'utilisateur
            email_str: Email
            password: Mot de passe en clair
            
        Returns:
            UserDTO: DTO de l'utilisateur créé
            
        Raises:
            ValueError: Si les données sont invalides
            DuplicateEmailError: Si l'email existe déjà
        """
        
        # 1. Créer les value objects
        email = Email(email_str)
        
        # 2. Vérifier l'unicité de l'email
        if self.user_repository.exists_by_email(email):
            raise ValueError("Email already exists")
        
        # 3. Vérifier l'unicité du username
        if self.user_repository.exists_by_username(username):
            raise ValueError("Username already exists")
        
        # 4. Créer l'entité User
        user = User(
            username=username,
            email=email,
            password_hash=User.hash_password(password)
        )
        
        # 5. Valider l'entité
        user.validate()
        
        # 6. Sauvegarder via le repository
        saved_user = self.user_repository.save(user)
        
        # 7. Envoyer l'email de bienvenue (asynchrone, non bloquant)
        try:
            self.email_gateway.send_welcome_email(
                to=email.value,
                username=username
            )
        except Exception as e:
            # Log l'erreur mais ne fait pas échouer la création
            print(f"Failed to send welcome email: {e}")
        
        # 8. Retourner le DTO
        return UserDTO.from_entity(saved_user)
```

**application/use_cases/order/create_order.py**

```python
from typing import List, Dict
from domain.entities.order import Order, OrderItem
from domain.value_objects.money import Money
from application.interfaces.repositories.order_repository import OrderRepository
from application.interfaces.repositories.product_repository import ProductRepository
from application.dtos.order_dto import OrderDTO

class CreateOrderInput:
    """Input pour créer une commande"""
    def __init__(self, user_id: int, items: List[Dict]):
        self.user_id = user_id
        self.items = items  # [{'product_id': 1, 'quantity': 2}, ...]

class CreateOrderUseCase:
    """Use Case : Créer une commande"""
    
    def __init__(
        self,
        order_repository: OrderRepository,
        product_repository: ProductRepository
    ):
        self.order_repository = order_repository
        self.product_repository = product_repository
    
    def execute(self, input_data: CreateOrderInput) -> OrderDTO:
        """
        Exécute le use case
        
        Args:
            input_data: Données d'entrée
            
        Returns:
            OrderDTO: Commande créée
            
        Raises:
            ValueError: Si les données sont invalides
            InsufficientStockError: Si stock insuffisant
        """
        
        # 1. Récupérer les produits et vérifier disponibilité
        order_items = []
        
        for item_data in input_data.items:
            product_id = item_data['product_id']
            quantity = item_data['quantity']
            
            # Récupérer le produit
            product = self.product_repository.find_by_id(product_id)
            if not product:
                raise ValueError(f"Product {product_id} not found")
            
            # Vérifier disponibilité
            if not product.is_available():
                raise ValueError(f"Product {product.name} is not available")
            
            # Vérifier le stock
            if product.stock < quantity:
                raise ValueError(
                    f"Insufficient stock for {product.name}. "
                    f"Available: {product.stock}, Requested: {quantity}"
                )
            
            # Créer l'OrderItem
            order_item = OrderItem(
                product_id=product.id,
                product_name=product.name,
                price=product.price,
                quantity=quantity
            )
            order_items.append(order_item)
        
        # 2. Créer l'entité Order
        order = Order(
            user_id=input_data.user_id,
            items=order_items
        )
        
        # 3. Valider la commande
        order.validate()
        
        # 4. Réduire le stock des produits
        for item_data in input_data.items:
            product_id = item_data['product_id']
            quantity = item_data['quantity']
            
            product = self.product_repository.find_by_id(product_id)
            product.reduce_stock(quantity)
            self.product_repository.update(product)
        
        # 5. Sauvegarder la commande
        saved_order = self.order_repository.save(order)
        
        # 6. Retourner le DTO
        return OrderDTO.from_entity(saved_order)
```

**application/interfaces/repositories/user_repository.py**

```python
from abc import ABC, abstractmethod
from typing import Optional
from domain.entities.user import User
from domain.value_objects.email import Email

class UserRepository(ABC):
    """
    Interface (Port) pour le repository User
    
    Cette interface définit le CONTRAT
    Les implémentations concrètes sont dans infrastructure/
    """
    
    @abstractmethod
    def save(self, user: User) -> User:
        """Sauvegarde un utilisateur"""
        pass
    
    @abstractmethod
    def find_by_id(self, user_id: int) -> Optional[User]:
        """Trouve un utilisateur par ID"""
        pass
    
    @abstractmethod
    def find_by_email(self, email: Email) -> Optional[User]:
        """Trouve un utilisateur par email"""
        pass
    
    @abstractmethod
    def find_by_username(self, username: str) -> Optional[User]:
        """Trouve un utilisateur par username"""
        pass
    
    @abstractmethod
    def exists_by_email(self, email: Email) -> bool:
        """Vérifie si un email existe"""
        pass
    
    @abstractmethod
    def exists_by_username(self, username: str) -> bool:
        """Vérifie si un username existe"""
        pass
    
    @abstractmethod
    def update(self, user: User) -> User:
        """Met à jour un utilisateur"""
        pass
    
    @abstractmethod
    def delete(self, user_id: int) -> bool:
        """Supprime un utilisateur"""
        pass
```

**application/dtos/user_dto.py**

```python
from dataclasses import dataclass
from datetime import datetime
from domain.entities.user import User

@dataclass
class UserDTO:
    """
    Data Transfer Object pour User
    
    Utilisé pour transférer des données entre couches
    sans exposer l'entité complète
    """
    
    id: int
    username: str
    email: str
    is_active: bool
    created_at: datetime
    
    @staticmethod
    def from_entity(user: User) -> 'UserDTO':
        """Crée un DTO depuis une entité"""
        return UserDTO(
            id=user.id,
            username=user.username,
            email=user.email.value,
            is_active=user.is_active,
            created_at=user.created_at
        )
    
    def to_dict(self) -> dict:
        """Convertit en dictionnaire"""
        return {
            'id': self.id,
            'username': self.username,
            'email': self.email,
            'is_active': self.is_active,
            'created_at': self.created_at.isoformat()
        }
```

---

### 3. INFRASTRUCTURE (Adaptateurs)

**infrastructure/repositories/sql_user_repository.py**

```python
from typing import Optional
from sqlalchemy.orm import Session
from domain.entities.user import User
from domain.value_objects.email import Email
from application.interfaces.repositories.user_repository import UserRepository
from infrastructure.database.models import UserModel

class SQLUserRepository(UserRepository):
    """
    Implémentation SQL du UserRepository
    
    Adaptateur concret qui implémente l'interface
    Remplaçable par MongoUserRepository, RedisUserRepository, etc.
    """
    
    def __init__(self, session: Session):
        self.session = session
    
    def save(self, user: User) -> User:
        """Sauvegarde un utilisateur"""
        # Convertir Entity -> Model (ORM)
        model = UserModel(
            username=user.username,
            email=user.email.value,
            password_hash=user.password_hash,
            is_active=user.is_active
        )
        
        self.session.add(model)
        self.session.commit()
        self.session.refresh(model)
        
        # Convertir Model -> Entity
        return self._model_to_entity(model)
    
    def find_by_id(self, user_id: int) -> Optional[User]:
        """Trouve un utilisateur par ID"""
        model = self.session.query(UserModel).filter_by(id=user_id).first()
        return self._model_to_entity(model) if model else None
    
    def find_by_email(self, email: Email) -> Optional[User]:
        """Trouve un utilisateur par email"""
        model = self.session.query(UserModel)\
                            .filter_by(email=email.value)\
                            .first()
        return self._model_to_entity(model) if model else None
    
    def find_by_username(self, username: str) -> Optional[User]:
        """Trouve un utilisateur par username"""
        model = self.session.query(UserModel)\
                            .filter_by(username=username)\
                            .first()
        return self._model_to_entity(model) if model else None
    
    def exists_by_email(self, email: Email) -> bool:
        """Vérifie si un email existe"""
        count = self.session.query(UserModel)\
                            .filter_by(email=email.value)\
                            .count()
        return count > 0
    
    def exists_by_username(self, username: str) -> bool:
        """Vérifie si un username existe"""
        count = self.session.query(UserModel)\
                            .filter_by(username=username)\
                            .count()
        return count > 0
    
    def update(self, user: User) -> User:
        """Met à jour un utilisateur"""
        model = self.session.query(UserModel).filter_by(id=user.id).first()
        
        if not model:
            raise ValueError(f"User {user.id} not found")
        
        model.username = user.username
        model.email = user.email.value
        model.is_active = user.is_active
        
        self.session.commit()
        self.session.refresh(model)
        
        return self._model_to_entity(model)
    
    def delete(self, user_id: int) -> bool:
        """Supprime un utilisateur"""
        model = self.session.query(UserModel).filter_by(id=user_id).first()
        
        if not model:
            return False
        
        self.session.delete(model)
        self.session.commit()
        
        return True
    
    def _model_to_entity(self, model: UserModel) -> User:
        """Convertit Model -> Entity"""
        return User(
            user_id=model.id,
            username=model.username,
            email=Email(model.email),
            password_hash=model.password_hash,
            created_at=model.created_at
        )
```

**infrastructure/gateways/smtp_email_gateway.py**

```python
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from application.interfaces.gateways.email_gateway import EmailGateway

class SMTPEmailGateway(EmailGateway):
    """
    Implémentation SMTP pour envoyer des emails
    
    Adaptateur concret qui implémente l'interface
    Remplaçable par SendGridEmailGateway, MailgunEmailGateway, etc.
    """
    
    def __init__(self, smtp_host: str, smtp_port: int, 
                 username: str, password: str):
        self.smtp_host = smtp_host
        self.smtp_port = smtp_port
        self.username = username
        self.password = password
    
    def send_welcome_email(self, to: str, username: str):
        """Envoie un email de bienvenue"""
        subject = f"Bienvenue {username} !"
        body = f"""
        Bonjour {username},
        
        Bienvenue sur notre plateforme !
        
        Votre compte a été créé avec succès.
        
        Cordialement,
        L'équipe
        """
        
        self._send_email(to, subject, body)
    
    def send_order_confirmation(self, to: str, order_id: int, total: float):
        """Envoie une confirmation de commande"""
        subject = f"Confirmation de commande #{order_id}"
        body = f"""
        Votre commande #{order_id} a été confirmée !
        
        Total : {total} USD
        
        Merci pour votre achat !
        """
        
        self._send_email(to, subject, body)
    
    def _send_email(self, to: str, subject: str, body: str):
        """Méthode interne pour envoyer un email"""
        try:
            # Créer le message
            msg = MIMEMultipart()
            msg['From'] = self.username
            msg['To'] = to
            msg['Subject'] = subject
            msg.attach(MIMEText(body, 'plain'))
            
            # Se connecter au serveur SMTP
            with smtplib.SMTP(self.smtp_host, self.smtp_port) as server:
                server.starttls()
                server.login(self.username, self.password)
                server.send_message(msg)
            
            print(f"Email sent to {to}: {subject}")
            
        except Exception as e:
            # En production, logger l'erreur
            print(f"Failed to send email: {e}")
            raise
```

**infrastructure/database/models.py**

```python
from sqlalchemy import Column, Integer, String, Boolean, DateTime, Numeric, ForeignKey, Text
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
from datetime import datetime

Base = declarative_base()

class UserModel(Base):
    """Modèle ORM pour User"""
    __tablename__ = 'users'
    
    id = Column(Integer, primary_key=True)
    username = Column(String(80), unique=True, nullable=False)
    email = Column(String(120), unique=True, nullable=False)
    password_hash = Column(String(255), nullable=False)
    is_active = Column(Boolean, default=True)
    created_at = Column(DateTime, default=datetime.utcnow)
    
    # Relations
    orders = relationship('OrderModel', back_populates='user')

class ProductModel(Base):
    """Modèle ORM pour Product"""
    __tablename__ = 'products'
    
    id = Column(Integer, primary_key=True)
    name = Column(String(200), nullable=False)
    description = Column(Text)
    price = Column(Numeric(10, 2), nullable=False)
    currency = Column(String(3), default='USD')
    stock = Column(Integer, default=0)
    is_active = Column(Boolean, default=True)

class OrderModel(Base):
    """Modèle ORM pour Order"""
    __tablename__ = 'orders'
    
    id = Column(Integer, primary_key=True)
    user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
    status = Column(String(20), default='pending')
    created_at = Column(DateTime, default=datetime.utcnow)
    paid_at = Column(DateTime, nullable=True)
    
    # Relations
    user = relationship('UserModel', back_populates='orders')
    items = relationship('OrderItemModel', back_populates='order')

class OrderItemModel(Base):
    """Modèle ORM pour OrderItem"""
    __tablename__ = 'order_items'
    
    id = Column(Integer, primary_key=True)
    order_id = Column(Integer, ForeignKey('orders.id'), nullable=False)
    product_id = Column(Integer, ForeignKey('products.id'), nullable=False)
    product_name = Column(String(200), nullable=False)
    price = Column(Numeric(10, 2), nullable=False)
    currency = Column(String(3), default='USD')
    quantity = Column(Integer, nullable=False)
    
    # Relations
    order = relationship('OrderModel', back_populates='items')
```

---

### 4. PRESENTATION (Controllers)

**presentation/api/routes/user_routes.py**

```python
from flask import Blueprint, request, jsonify
from application.use_cases.user.create_user import CreateUserUseCase
from application.use_cases.user.authenticate_user import AuthenticateUserUseCase
from config.dependency_injection import get_container

user_bp = Blueprint('users', __name__, url_prefix='/api/users')

@user_bp.route('/', methods=['POST'])
def create_user():
    """
    Créer un utilisateur
    
    POST /api/users/
    Body: {"username": "john", "email": "john@example.com", "password": "secret"}
    """
    try:
        # Récupérer les données
        data = request.get_json()
        username = data.get('username')
        email = data.get('email')
        password = data.get('password')
        
        # Validation basique
        if not all([username, email, password]):
            return jsonify({'error': 'Missing required fields'}), 400
        
        # Récupérer le use case depuis le container DI
        container = get_container()
        use_case = container.get(CreateUserUseCase)
        
        # Exécuter le use case
        user_dto = use_case.execute(username, email, password)
        
        # Retourner la réponse
        return jsonify({
            'message': 'User created successfully',
            'user': user_dto.to_dict()
        }), 201
        
    except ValueError as e:
        return jsonify({'error': str(e)}), 400
    except Exception as e:
        return jsonify({'error': 'Internal server error'}), 500

@user_bp.route('/login', methods=['POST'])
def login():
    """
    Connexion
    
    POST /api/users/login
    Body: {"email": "john@example.com", "password": "secret"}
    """
    try:
        data = request.get_json()
        email = data.get('email')
        password = data.get('password')
        
        if not all([email, password]):
            return jsonify({'error': 'Missing credentials'}), 400
        
        # Use case
        container = get_container()
        use_case = container.get(AuthenticateUserUseCase)
        
        # Authentifier
        result = use_case.execute(email, password)
        
        if result:
            return jsonify({
                'message': 'Login successful',
                'token': result['token'],
                'user': result['user'].to_dict()
            }), 200
        else:
            return jsonify({'error': 'Invalid credentials'}), 401
            
    except Exception as e:
        return jsonify({'error': 'Internal server error'}), 500
```

---

### 5. CONFIGURATION (Dependency Injection)

**config/dependency_injection.py**

```python
from dependency_injector import containers, providers
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

# Use Cases
from application.use_cases.user.create_user import CreateUserUseCase
from application.use_cases.order.create_order import CreateOrderUseCase

# Repositories
from infrastructure.repositories.sql_user_repository import SQLUserRepository
from infrastructure.repositories.sql_product_repository import SQLProductRepository
from infrastructure.repositories.sql_order_repository import SQLOrderRepository

# Gateways
from infrastructure.gateways.smtp_email_gateway import SMTPEmailGateway

class Container(containers.DeclarativeContainer):
    """Container d'injection de dépendances"""
    
    # Configuration
    config = providers.Configuration()
    
    # Database
    engine = providers.Singleton(
        create_engine,
        config.database.url,
        echo=config.database.echo
    )
    
    session_factory = providers.Singleton(
        sessionmaker,
        bind=engine
    )
    
    session = providers.Factory(
        session_factory
    )
    
    # Repositories
    user_repository = providers.Factory(
        SQLUserRepository,
        session=session
    )
    
    product_repository = providers.Factory(
        SQLProductRepository,
        session=session
    )
    
    order_repository = providers.Factory(
        SQLOrderRepository,
        session=session
    )
    
    # Gateways
    email_gateway = providers.Singleton(
        SMTPEmailGateway,
        smtp_host=config.email.smtp_host,
        smtp_port=config.email.smtp_port,
        username=config.email.username,
        password=config.email.password
    )
    
    # Use Cases
    create_user_use_case = providers.Factory(
        CreateUserUseCase,
        user_repository=user_repository,
        email_gateway=email_gateway
    )
    
    create_order_use_case = providers.Factory(
        CreateOrderUseCase,
        order_repository=order_repository,
        product_repository=product_repository
    )

# Instance globale
_container = None

def get_container() -> Container:
    """Retourne le container DI"""
    global _container
    if _container is None:
        _container = Container()
        _container.config.from_yaml('config/settings.yaml')
    return _container
```

---

## [TEST] TESTS EN CLEAN ARCHITECTURE

### Tests unitaires (Entities)

```python
# tests/unit/domain/test_order_entity.py
import pytest
from domain.entities.order import Order, OrderItem
from domain.value_objects.money import Money

def test_order_calculate_total():
    """Test : Calcul du total de commande"""
    # Arrange
    items = [
        OrderItem(1, "Product 1", Money(10, 'USD'), 2),  # 20
        OrderItem(2, "Product 2", Money(15, 'USD'), 1),  # 15
    ]
    order = Order(user_id=1, items=items)
    
    # Act
    total = order.calculate_total()
    
    # Assert
    assert total.amount == 35
    assert total.currency == 'USD'

def test_order_free_shipping_above_100():
    """Test : Livraison gratuite au-dessus de 100"""
    items = [
        OrderItem(1, "Expensive", Money(120, 'USD'), 1)
    ]
    order = Order(user_id=1, items=items)
    
    shipping = order.calculate_shipping()
    
    assert shipping.amount == 0

def test_order_paid_shipping_below_100():
    """Test : Frais de livraison sous 100"""
    items = [
        OrderItem(1, "Cheap", Money(50, 'USD'), 1)
    ]
    order = Order(user_id=1, items=items)
    
    shipping = order.calculate_shipping()
    
    assert shipping.amount == 10
```

---

### Tests unitaires (Use Cases avec mocks)

```python
# tests/unit/application/test_create_user_use_case.py
import pytest
from unittest.mock import Mock
from application.use_cases.user.create_user import CreateUserUseCase
from domain.entities.user import User
from domain.value_objects.email import Email

def test_create_user_success():
    """Test : Création d'utilisateur réussie"""
    # Arrange
    mock_repo = Mock()
    mock_repo.exists_by_email.return_value = False
    mock_repo.exists_by_username.return_value = False
    mock_repo.save.return_value = User(
        user_id=1,
        username='john',
        email=Email('john@example.com'),
        password_hash='hashed'
    )
    
    mock_email_gateway = Mock()
    
    use_case = CreateUserUseCase(mock_repo, mock_email_gateway)
    
    # Act
    user_dto = use_case.execute('john', 'john@example.com', 'password123')
    
    # Assert
    assert user_dto.username == 'john'
    assert user_dto.email == 'john@example.com'
    mock_repo.save.assert_called_once()
    mock_email_gateway.send_welcome_email.assert_called_once()

def test_create_user_duplicate_email():
    """Test : Email déjà utilisé"""
    # Arrange
    mock_repo = Mock()
    mock_repo.exists_by_email.return_value = True  # Email existe déjà
    
    mock_email_gateway = Mock()
    
    use_case = CreateUserUseCase(mock_repo, mock_email_gateway)
    
    # Act & Assert
    with pytest.raises(ValueError, match="Email already exists"):
        use_case.execute('john', 'john@example.com', 'password123')
    
    # Vérifier que save n'a PAS été appelé
    mock_repo.save.assert_not_called()
```

---

### Tests d'intégration (Repositories)

```python
# tests/integration/infrastructure/test_sql_user_repository.py
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from infrastructure.database.models import Base, UserModel
from infrastructure.repositories.sql_user_repository import SQLUserRepository
from domain.entities.user import User
from domain.value_objects.email import Email

@pytest.fixture
def test_db():
    """Fixture : DB de test en mémoire"""
    engine = create_engine('sqlite:///:memory:')
    Base.metadata.create_all(engine)
    Session = sessionmaker(bind=engine)
    session = Session()
    yield session
    session.close()

def test_save_user(test_db):
    """Test : Sauvegarder un utilisateur"""
    # Arrange
    repo = SQLUserRepository(test_db)
    user = User(
        username='john',
        email=Email('john@example.com'),
        password_hash='hashed'
    )
    
    # Act
    saved_user = repo.save(user)
    
    # Assert
    assert saved_user.id is not None
    assert saved_user.username == 'john'
    
    # Vérifier en DB
    db_user = test_db.query(UserModel).filter_by(username='john').first()
    assert db_user is not None
    assert db_user.email == 'john@example.com'

def test_find_by_email(test_db):
    """Test : Trouver par email"""
    # Arrange
    repo = SQLUserRepository(test_db)
    user = User(
        username='jane',
        email=Email('jane@example.com'),
        password_hash='hashed'
    )
    repo.save(user)
    
    # Act
    found_user = repo.find_by_email(Email('jane@example.com'))
    
    # Assert
    assert found_user is not None
    assert found_user.username == 'jane'
```

---

## [OBJECTIF] EXERCICES PRATIQUES

### Exercice 1 : Ajouter un Use Case **

**Créer un use case "UpdateUserProfile"**

**Fonctionnalités :**
- Modifier username
- Modifier email (avec vérification d'unicité)
- Valider les données

**Fichiers à créer :**
```
application/use_cases/user/update_profile.py
tests/unit/application/test_update_profile.py
```

---

### Exercice 2 : Ajouter un nouveau Repository ***

**Créer une implémentation MongoDB du UserRepository**

**Tâches :**
1. Créer `infrastructure/repositories/mongo_user_repository.py`
2. Implémenter toutes les méthodes de l'interface
3. Tester avec une DB MongoDB de test

**Objectif : Montrer que le repository est interchangeable**

---

### Exercice 3 : Système de wishlist ****

**Ajouter une wishlist (liste de souhaits)**

**Entities :**
- `Wishlist` (user_id, products)

**Use Cases :**
- `AddProductToWishlist`
- `RemoveProductFromWishlist`
- `GetUserWishlist`

**Structure complète :**
```
domain/entities/wishlist.py
application/use_cases/wishlist/
infrastructure/repositories/wishlist_repository.py
presentation/api/routes/wishlist_routes.py
tests/
```

---

## [BRAVO] CONCLUSION

### Ce que tu as appris

**Architecture Clean :**
- [OK] Les 4 couches (Entities, Use Cases, Adapters, Frameworks)
- [OK] Règle de dépendance (vers l'intérieur)
- [OK] Indépendance totale du business logic
- [OK] Testabilité maximale

**Pratique :**
- [OK] Code complet d'un e-commerce
- [OK] Entities, Use Cases, Repositories, Controllers
- [OK] Dependency Injection
- [OK] Tests à tous les niveaux

---

### Points clés

```
[OBJECTIF] Clean Architecture = Indépendance maximale
[OBJECTIF] Centre = Logique métier pure
[OBJECTIF] Extérieur = Détails techniques (remplaçables)
[OBJECTIF] Tests faciles à tous les niveaux
[OBJECTIF] Maintenance simplifiée
[OBJECTIF] Architecture professionnelle moderne
```

---

### Quand utiliser Clean Architecture ?

```
[OK] OUI :
- Projet à long terme (> 6 mois)
- Équipe > 3 développeurs
- Besoin de tests robustes
- Changements fréquents de technologie
- Application d'entreprise critique

[X] NON :
- Prototypes rapides
- Scripts one-shot
- Projets < 1 mois
- Équipe solo sur un petit projet
```

---

### Prochaines étapes

1. Code l'exercice Wishlist
2. Compare avec MVC (différences)
3. Lis `architecture_hexagonale.txt` (similaire à Clean)
4. Lis `architecture_microservices.txt`

**[RAPIDE] Félicitations ! Tu maîtrises Clean Architecture ! [RAPIDE]**

═══════════════════════════════════════════════════════════════
FIN DU FICHIER : architecture_clean.txt
═══════════════════════════════════════════════════════════════

# [WEB] ARCHITECTURE MICROSERVICES - GUIDE ULTRA DÉTAILLÉ

## * INTRODUCTION

### Qu'est-ce que les Microservices ?

**Microservices** = Architecture où l'application est **divisée en petits services indépendants**, chacun exécutant une **fonction métier spécifique**.

**Analogie : Ville vs Village [CITYSCAPE]**

```
[ENTREPRISE] MONOLITHE = Grand immeuble
   ├─ Tout dans UN bâtiment
   ├─ Si le bâtiment tombe -> Tout s'arrête
   ├─ Difficile de rénover une partie
   └─ Tout le monde dépend du même ascenseur

[HOUSE_BUILDINGS] MICROSERVICES = Ville avec plein de maisons
   ├─ Chaque maison = service indépendant
   ├─ Si une maison tombe -> Les autres continuent
   ├─ Facile de rénover/remplacer une maison
   └─ Chaque maison a son propre accès
```

**En code :**

```
[X] MONOLITHE (tout ensemble) :
┌─────────────────────────────────┐
│  APPLICATION UNIQUE             │
│  ├─ Authentification            │
│  ├─ Produits                    │
│  ├─ Commandes                   │
│  ├─ Paiement                    │
│  └─ Notifications               │
│                                 │
│  [PACKAGE] Un seul déploiement         │
│  [ARCHIVE] Une seule base de données   │
└─────────────────────────────────┘

[OK] MICROSERVICES (séparés) :
┌──────────────┐  ┌──────────────┐  ┌──────────────┐
│   SERVICE    │  │   SERVICE    │  │   SERVICE    │
│     AUTH     │  │   PRODUITS   │  │  COMMANDES   │
│              │  │              │  │              │
│ DB Auth      │  │ DB Produits  │  │ DB Orders    │
└──────────────┘  └──────────────┘  └──────────────┘
       v                 v                 v
    Port 8001        Port 8002         Port 8003
    
┌──────────────┐  ┌──────────────┐
│   SERVICE    │  │   SERVICE    │
│   PAIEMENT   │  │    NOTIF     │
│              │  │              │
│ DB Payment   │  │ Queue/Redis  │
└──────────────┘  └──────────────┘
       v                 v
    Port 8004        Port 8005

         ^ Tous communiquent via réseau ^
```

---

### [GRAPHIQUE] Caractéristiques principales

| Caractéristique | Description |
|-----------------|-------------|
| **Indépendance** | Chaque service est autonome |
| **Déploiement séparé** | Déployer un service sans toucher les autres |
| **Base de données par service** | Chaque service a SA propre DB |
| **Communication** | Via réseau (HTTP/REST, gRPC, messages) |
| **Technologie diverse** | Chaque service peut utiliser un langage différent |
| **Scalabilité fine** | Scaler uniquement les services nécessaires |

---

### [OBJECTIF] Principes fondamentaux

**1. Single Responsibility (Responsabilité unique)**

```
Chaque microservice = UNE fonction métier

[OK] BON :
- Service "User Authentication" -> Gérer l'auth uniquement
- Service "Product Catalog" -> Gérer les produits uniquement
- Service "Order Processing" -> Gérer les commandes uniquement

[X] MAUVAIS :
- Service "Everything" -> Auth + Produits + Commandes
  (C'est un monolithe déguisé !)
```

---

**2. Autonomie (Independence)**

```
Chaque service peut :
[OK] Être développé indépendamment
[OK] Être déployé indépendamment
[OK] Être scalé indépendamment
[OK] Utiliser sa propre technologie
[OK] Avoir sa propre base de données

Exemple :
Service Auth      -> Python + PostgreSQL
Service Products  -> Node.js + MongoDB
Service Orders    -> Go + MySQL
Service Payments  -> Java + PostgreSQL
```

---

**3. Communication décentralisée**

```
Les services communiquent via :

[RESEAU] Synchrone (Requête/Réponse) :
   - HTTP/REST
   - gRPC
   - GraphQL

[EMAIL] Asynchrone (Messages) :
   - RabbitMQ
   - Kafka
   - Redis Pub/Sub
   - AWS SQS

Pas d'accès direct aux bases de données des autres services !
```

---

**4. Base de données par service**

```
[X] MAUVAIS (DB partagée) :
Service Auth   ─┐
Service Orders ─┼─->  [PACKAGE] BASE DE DONNÉES COMMUNE
Service Payment─┘     (Couplage fort !)

[OK] BON (DB par service) :
Service Auth     -> [PACKAGE] DB Auth
Service Orders   -> [PACKAGE] DB Orders
Service Payment  -> [PACKAGE] DB Payment
(Indépendance totale !)
```

---

## [REFLEXION] POURQUOI UTILISER LES MICROSERVICES ?

### [OK] Avantages

**1. Scalabilité indépendante [HAUSSE]**

**Scénario : Application de streaming vidéo**

```
Charge typique :
├─ Authentification : 1,000 req/sec
├─ Catalogue : 5,000 req/sec
├─ Upload : 100 req/sec
└─ Streaming : 100,000 req/sec <- ÉNORME !

Avec MONOLITHE :
Il faut scaler TOUTE l'application
├─ Serveur 1 : Auth + Catalogue + Upload + Streaming
├─ Serveur 2 : Auth + Catalogue + Upload + Streaming
├─ Serveur 3 : Auth + Catalogue + Upload + Streaming
└─ ...

[X] Gaspillage : On paie pour Auth/Catalogue/Upload qu'on n'utilise pas
[ARGENT] Coût : 50 serveurs × 200€ = 10,000€/mois

Avec MICROSERVICES :
On scale UNIQUEMENT le streaming
├─ Auth : 1 serveur (50€/mois)
├─ Catalogue : 2 serveurs (100€/mois)
├─ Upload : 1 serveur (50€/mois)
└─ Streaming : 40 serveurs (8,000€/mois)

[OK] Optimal : On paie pour ce qu'on utilise
[ARGENT] Coût : 8,200€/mois (18% moins cher)
```

**Exemple Netflix :**
- Service de recommendation : 500+ serveurs
- Service d'authentification : 10 serveurs
- Chaque service scale selon ses besoins

---

**2. Développement parallèle [UTILISATEURS]**

**Avec MONOLITHE :**

```
Équipe de 30 développeurs sur 1 code base

Problèmes :
[X] Conflits Git constants
[X] Attente pour les merges
[X] Tests qui cassent mutuellement
[X] Déploiements coordonnés (cauchemar)
[X] Pas de propriété claire du code

Résultat : Productivité -> [BAISSE]
```

**Avec MICROSERVICES :**

```
30 développeurs = 10 équipes de 3 personnes

Équipe 1 : Service Auth (code autonome)
Équipe 2 : Service Products (code autonome)
Équipe 3 : Service Orders (code autonome)
...

Avantages :
[OK] Zéro conflit entre équipes
[OK] Déploiements indépendants
[OK] Propriété claire (chaque équipe = son service)
[OK] Spécialisation (experts du domaine)
[OK] Productivité -> [HAUSSE]

Exemple Amazon :
"Two-pizza teams" (équipe nourrie avec 2 pizzas)
-> Petites équipes autonomes par service
```

---

**3. Résilience (Fault Isolation) [SECURITE]**

**Avec MONOLITHE :**

```
Scénario : Bug dans le module de paiement

┌─────────────────────────────────┐
│         MONOLITHE               │
│  ├─ Auth [OK]                      │
│  ├─ Products [OK]                  │
│  ├─ Orders [OK]                    │
│  ├─ Payment [IMPACT] (CRASH)           │
│  └─ Notifications [OK]             │
└─────────────────────────────────┘
           v
   TOUT L'APP CRASH [IMPACT]
   
[X] Site complètement down
[X] Perte de revenus totale
[X] Clients mécontents
```

**Avec MICROSERVICES :**

```
Scénario : Bug dans le service de paiement

Service Auth        [OK] (fonctionne)
Service Products    [OK] (fonctionne)
Service Orders      [OK] (fonctionne)
Service Payment     [IMPACT] (crash)
Service Notif       [OK] (fonctionne)
           v
Dégradation gracieuse :
[OK] Les clients peuvent naviguer
[OK] Les clients peuvent ajouter au panier
[OK] Impossible de payer (temporaire)
[OK] Message : "Paiement temporairement indisponible"

Résultat :
[OK] 90% du site fonctionne
[OK] Perte de revenus partielle uniquement
[OK] Meilleure expérience client
```

**Circuit Breaker pattern :**

```python
class PaymentService:
    def process_payment(self, order):
        try:
            # Appeler le service de paiement
            response = requests.post('http://payment-service/pay', 
                                    json=order, 
                                    timeout=3)
            return response.json()
        except requests.exceptions.Timeout:
            # Circuit breaker : Service down
            # Fallback : Mettre en queue pour traitement ultérieur
            queue.add(order)
            return {
                'status': 'pending',
                'message': 'Payment will be processed shortly'
            }
```

---

**4. Liberté technologique [OUTIL]**

**Avec MONOLITHE :**

```
Toute l'application en Python

Service Auth       -> Python [OK]
Service Products   -> Python [ATTENTION] (pas optimal)
Service ML         -> Python [OK]
Service Temps réel -> Python [X] (lent)
Service Analytics  -> Python [ATTENTION] (pas le meilleur)

Problème : Coincé avec Python partout
```

**Avec MICROSERVICES :**

```
Chaque service = Meilleure techno pour le job

Service Auth          -> Python (Django) - Rapide à développer
Service Products      -> Node.js - Bon pour APIs
Service ML            -> Python (TensorFlow) - Conçu pour ça
Service Temps réel    -> Go - Ultra rapide
Service Analytics     -> Scala (Spark) - Big Data
Service Video Encoding-> C++ - Performance maximale

[OK] Chaque service optimisé pour sa fonction
[OK] Adopter nouvelles technos facilement
[OK] Experts peuvent utiliser leurs outils préférés
```

**Exemple Uber :**
- 2,200+ microservices
- Python, Go, Java, Node.js, etc.
- Chaque service dans la meilleure techno

---

**5. Déploiement continu facile [RAPIDE]**

**Avec MONOLITHE :**

```
Modification dans 1 ligne de code
    v
Build de TOUTE l'application (15 min)
    v
Tests de TOUTE l'application (30 min)
    v
Déploiement de TOUT (10 min)
    v
Si erreur -> Rollback de TOUT
    v
Total : 55 minutes minimum par déploiement

Déploiements : 2-3 fois par semaine maximum
```

**Avec MICROSERVICES :**

```
Modification dans le service "Orders"
    v
Build du service Orders uniquement (2 min)
    v
Tests du service Orders uniquement (5 min)
    v
Déploiement du service Orders (2 min)
    v
Les autres services continuent de fonctionner
    v
Total : 9 minutes

Déploiements : Plusieurs fois par jour, par service

Exemple Amazon :
- 50 millions de déploiements par an
- Déploiement toutes les 11.7 secondes (moyenne)
```

---

**6. Facilité de compréhension [DOCS]**

**Avec MONOLITHE (500,000 lignes) :**

```
Nouveau développeur :
├─ Cloner le repo (10 GB)
├─ Installer 50 dépendances
├─ Comprendre 500,000 lignes
├─ Trouver où modifier
├─ Temps d'onboarding : 3-6 mois
└─ Peur de casser quelque chose
```

**Avec MICROSERVICES :**

```
Nouveau développeur :
├─ Cloner 1 service (50 MB)
├─ Installer 5 dépendances
├─ Comprendre 5,000 lignes
├─ Scope clair et limité
├─ Temps d'onboarding : 1-2 semaines
└─ Confiance pour modifier

Focus : "Je travaille sur le service Orders"
       (pas besoin de comprendre les 99 autres services)
```

---

### [X] Inconvénients

**1. Complexité opérationnelle [SHOCKED_FACE_WITH_EXPLODING_HEAD]**

**Monolithe (simple) :**

```
Déploiement :
├─ 1 serveur
├─ 1 base de données
├─ 1 application
└─ Monitoring simple

Total à gérer : 3 composants
```

**Microservices (complexe) :**

```
Déploiement (exemple petit e-commerce) :
├─ 10 microservices
├─ 10 bases de données
├─ 1 API Gateway
├─ 1 Service Discovery
├─ 1 Load Balancer
├─ 1 Message Queue
├─ 1 Monitoring (Prometheus)
├─ 1 Logging (ELK Stack)
├─ 1 Tracing (Jaeger)
└─ Container Orchestration (Kubernetes)

Total à gérer : 30+ composants [!]

Compétences requises :
- Docker
- Kubernetes
- Networking
- Distributed systems
- DevOps
- Monitoring/Alerting
- Security
```

---

**2. Latence réseau [TEMPS]**

**Monolithe (appels locaux) :**

```python
# Tout dans la même application
def create_order(user_id, product_id):
    user = get_user(user_id)           # 0.001 ms (mémoire)
    product = get_product(product_id)  # 0.001 ms (mémoire)
    order = save_order(user, product)  # 1 ms (DB locale)
    send_email(user.email)             # 0.001 ms (appel fonction)
    
    return order

Total : ~2 ms [RAPIDE]
```

**Microservices (appels réseau) :**

```python
# Services séparés
def create_order(user_id, product_id):
    # Appel HTTP au service User
    user = requests.get(f'http://user-service/users/{user_id}')  # 50 ms
    
    # Appel HTTP au service Product
    product = requests.get(f'http://product-service/products/{product_id}')  # 50 ms
    
    # Appel HTTP au service Order
    order = requests.post('http://order-service/orders', json={...})  # 50 ms
    
    # Appel HTTP au service Email
    requests.post('http://email-service/send', json={...})  # 50 ms
    
    return order

Total : ~200 ms [LENT] (100x plus lent !)
```

**Solutions :**
- Caching agressif
- Calls asynchrones
- Batching de requêtes
- gRPC (plus rapide que REST)

---

**3. Transactions distribuées [CARTE]**

**Problème des transactions ACID :**

**Monolithe (facile) :**

```python
# Transaction atomique dans une seule DB
def create_order_with_payment(user_id, product_id, payment_info):
    with db.transaction():  # Si erreur, TOUT est rollback automatiquement
        # 1. Vérifier stock
        product = Product.get(product_id)
        if product.stock < 1:
            raise InsufficientStock()
        
        # 2. Créer commande
        order = Order.create(user_id, product_id)
        
        # 3. Débiter le compte
        Payment.charge(user_id, product.price)
        
        # 4. Réduire le stock
        product.reduce_stock(1)
        
        # [OK] Tout réussit ou tout échoue (atomique)
        return order
```

**Microservices (complexe) :**

```python
# Chaque appel = service différent (DB différente)
def create_order_with_payment(user_id, product_id, payment_info):
    # 1. Vérifier stock (Service Product)
    response = requests.get(f'http://product-service/products/{product_id}')
    if response.json()['stock'] < 1:
        raise InsufficientStock()
    
    # 2. Créer commande (Service Order)
    order_response = requests.post('http://order-service/orders', json={...})
    order_id = order_response.json()['id']
    
    # 3. Débiter le compte (Service Payment)
    payment_response = requests.post('http://payment-service/charge', json={...})
    
    # [X] PROBLÈME : Le paiement échoue !
    if not payment_response.ok:
        # Il faut annuler manuellement la commande
        requests.delete(f'http://order-service/orders/{order_id}')
        # Mais si cette requête échoue aussi ? [!]
        raise PaymentFailed()
    
    # 4. Réduire le stock (Service Product)
    stock_response = requests.put(f'http://product-service/products/{product_id}/reduce')
    
    # [X] PROBLÈME : La réduction de stock échoue !
    if not stock_response.ok:
        # Il faut annuler la commande ET le paiement
        requests.delete(f'http://order-service/orders/{order_id}')
        requests.post('http://payment-service/refund', json={...})
        # Et si ces requêtes échouent ? [!][!]
        raise StockUpdateFailed()
    
    return order_id

# [SHOCKED_FACE_WITH_EXPLODING_HEAD] Cauchemar de gestion d'erreurs !
```

**Solution : Saga Pattern**

```python
# Pattern Saga (orchestration)
class CreateOrderSaga:
    def execute(self, user_id, product_id, payment_info):
        steps = []
        
        try:
            # Étape 1 : Créer commande
            order = self.order_service.create(user_id, product_id)
            steps.append(('order', order.id))
            
            # Étape 2 : Débiter
            payment = self.payment_service.charge(user_id, ...)
            steps.append(('payment', payment.id))
            
            # Étape 3 : Réduire stock
            self.product_service.reduce_stock(product_id, 1)
            steps.append(('stock', product_id))
            
            return order
            
        except Exception as e:
            # Rollback de toutes les étapes effectuées
            self.rollback(steps)
            raise
    
    def rollback(self, steps):
        # Annuler dans l'ordre inverse
        for step_type, step_id in reversed(steps):
            if step_type == 'order':
                self.order_service.cancel(step_id)
            elif step_type == 'payment':
                self.payment_service.refund(step_id)
            elif step_type == 'stock':
                self.product_service.increase_stock(step_id, 1)
```

---

**4. Data consistency (Cohérence des données) [SYNC]**

**Problème : Chaque service a SA propre DB**

```
Scénario : Mettre à jour l'email d'un utilisateur

Service User a l'email -> john@old.com
Service Order a l'email (copie) -> john@old.com
Service Notification a l'email (copie) -> john@old.com

L'utilisateur change son email -> john@new.com

Service User est mis à jour -> john@new.com [OK]

Mais :
Service Order a toujours -> john@old.com [X]
Service Notification a toujours -> john@old.com [X]

[IMPACT] INCOHÉRENCE DES DONNÉES !
```

**Solutions :**

**1. Event-Driven Architecture**

```python
# Service User publie un événement
class UserService:
    def update_email(self, user_id, new_email):
        user = self.repository.find(user_id)
        user.email = new_email
        self.repository.save(user)
        
        # Publier un événement
        event_bus.publish('user.email.updated', {
            'user_id': user_id,
            'old_email': user.email,
            'new_email': new_email
        })

# Service Order écoute l'événement
class OrderService:
    @event_handler('user.email.updated')
    def on_user_email_updated(self, event):
        user_id = event['user_id']
        new_email = event['new_email']
        
        # Mettre à jour localement
        self.repository.update_user_email(user_id, new_email)

# Service Notification écoute aussi
class NotificationService:
    @event_handler('user.email.updated')
    def on_user_email_updated(self, event):
        # Mettre à jour localement
        ...

[OK] Eventual Consistency (cohérence éventuelle)
```

**2. Ne pas dupliquer les données**

```python
# Au lieu de stocker l'email partout
# Faire une requête au service User quand nécessaire

class OrderService:
    def send_order_confirmation(self, order_id):
        order = self.repository.find(order_id)
        
        # Récupérer l'email à jour depuis le service User
        user_response = requests.get(f'http://user-service/users/{order.user_id}')
        user_email = user_response.json()['email']
        
        self.email_service.send(user_email, "Order confirmation", ...)
```

---

**5. Testing complexe [TEST]**

**Monolithe (simple) :**

```python
# Tests end-to-end
def test_create_order():
    # 1. Démarrer l'application
    app = start_app()
    
    # 2. Tester
    response = client.post('/orders', json={...})
    
    # 3. Vérifier
    assert response.status_code == 201

[OK] Facile : Tout dans une seule app
```

**Microservices (complexe) :**

```python
# Tests end-to-end
def test_create_order():
    # 1. Démarrer TOUS les services (10+)
    start_user_service()
    start_product_service()
    start_order_service()
    start_payment_service()
    start_email_service()
    start_api_gateway()
    start_database_cluster()
    start_message_queue()
    # ...
    
    # 2. Attendre que tout soit prêt
    wait_for_services()
    
    # 3. Tester
    response = client.post('http://api-gateway/orders', json={...})
    
    # 4. Vérifier dans plusieurs services
    assert response.status_code == 201
    assert order_service.has_order(order_id)
    assert payment_service.has_payment(order_id)
    assert email_service.sent_email(user_email)

[!] Lent, fragile, difficile à maintenir
```

**Solutions :**
- Contract Testing (Pact)
- Service Virtualization (Mocks)
- Consumer-Driven Contracts

---

**6. Coût initial élevé [ARGENT]**

**Monolithe :**

```
Coûts initiaux :
├─ 1 serveur : 50€/mois
├─ 1 base de données : 30€/mois
├─ Temps de développement : 2 semaines
└─ Compétences requises : Backend basique

Total initial : 80€/mois + 2 semaines de dev
```

**Microservices :**

```
Coûts initiaux :
├─ 5-10 serveurs (un par service) : 300€/mois
├─ 5-10 bases de données : 200€/mois
├─ API Gateway : 50€/mois
├─ Load Balancer : 30€/mois
├─ Message Queue (RabbitMQ/Kafka) : 50€/mois
├─ Monitoring (Prometheus/Grafana) : 50€/mois
├─ Logging (ELK Stack) : 100€/mois
├─ Container Orchestration (Kubernetes) : 200€/mois
├─ Temps de développement : 8-12 semaines
└─ Compétences requises : Docker, K8s, DevOps, Distributed Systems

Total initial : 980€/mois + 12 semaines de dev

[ARGENT] 12x plus cher + 6x plus de temps !
```

---

## [CONSTRUCTION] ARCHITECTURE COMPLÈTE D'UN SYSTÈME MICROSERVICES

### [MESURE] Composants d'une architecture microservices

```
┌─────────────────────────────────────────────────────────────┐
│                    CLIENT (Web/Mobile)                      │
└──────────────────────┬──────────────────────────────────────┘
                       │
                       v
┌──────────────────────────────────────────────────────────────┐
│                   [SORTIE] API GATEWAY                             │
│  ├─ Routing                                                  │
│  ├─ Authentication                                           │
│  ├─ Rate Limiting                                            │
│  └─ Load Balancing                                           │
└────┬───────────┬───────────┬───────────┬──────────┬─────────┘
     │           │           │           │          │
     v           v           v           v          v
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ SERVICE │ │ SERVICE │ │ SERVICE │ │ SERVICE │ │ SERVICE │
│  AUTH   │ │PRODUCTS │ │ ORDERS  │ │ PAYMENT │ │  NOTIF  │
│         │ │         │ │         │ │         │ │         │
│ DB Auth │ │ DB Prod │ │ DB Order│ │ DB Pay  │ │ Queue   │
└────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘
     │           │           │           │          │
     └───────────┴───────────┴───────────┴──────────┘
                       │
                       v
          ┌────────────────────────┐
          │   MESSAGE BUS/QUEUE    │
          │   (RabbitMQ/Kafka)     │
          └────────────────────────┘
                       │
                       v
          ┌────────────────────────┐
          │  SERVICE DISCOVERY     │
          │  (Consul/Eureka)       │
          └────────────────────────┘
                       │
                       v
          ┌────────────────────────┐
          │  MONITORING & LOGGING  │
          │  (Prometheus/ELK)      │
          └────────────────────────┘
```

---

### [OUTIL] Composants détaillés

**1. API Gateway [SORTIE]**

```
Rôle : Point d'entrée unique pour tous les clients

Responsabilités :
├─ Routing : Diriger les requêtes vers le bon service
├─ Authentication : Vérifier les tokens JWT
├─ Rate Limiting : Limiter les requêtes (anti-spam)
├─ Load Balancing : Répartir la charge
├─ Request/Response transformation
├─ Caching
└─ Monitoring

Exemple avec Kong/Nginx :
GET /users/123 -> Route vers service User
GET /products/456 -> Route vers service Product
POST /orders -> Route vers service Order
```

**Code exemple (Python - Flask):**

```python
# api_gateway.py
from flask import Flask, request, jsonify
import requests

app = Flask(__name__)

# Configuration des services
SERVICES = {
    'user': 'http://user-service:8001',
    'product': 'http://product-service:8002',
    'order': 'http://order-service:8003'
}

@app.route('/<service>/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE'])
def gateway(service, path):
    """Gateway qui route vers les microservices"""
    
    # 1. Vérifier l'authentification
    token = request.headers.get('Authorization')
    if not token:
        return jsonify({'error': 'Unauthorized'}), 401
    
    # 2. Vérifier que le service existe
    if service not in SERVICES:
        return jsonify({'error': 'Service not found'}), 404
    
    # 3. Construire l'URL du service
    service_url = f"{SERVICES[service]}/{path}"
    
    # 4. Transférer la requête
    try:
        response = requests.request(
            method=request.method,
            url=service_url,
            headers={k: v for k, v in request.headers if k != 'Host'},
            data=request.get_data(),
            timeout=5
        )
        
        return response.content, response.status_code
        
    except requests.exceptions.Timeout:
        return jsonify({'error': 'Service timeout'}), 504
    except requests.exceptions.ConnectionError:
        return jsonify({'error': 'Service unavailable'}), 503

if __name__ == '__main__':
    app.run(port=8000)
```

---

**2. Service Discovery [RECHERCHE]**

```
Rôle : Permettre aux services de se trouver dynamiquement

Problème sans Service Discovery :
Service Order veut appeler Service User
-> Il faut connaître l'IP : http://192.168.1.10:8001
-> Si l'IP change (scaling, redémarrage) -> [X] Cassé

Solution avec Service Discovery :
Service Order demande à Consul/Eureka : "Où est User Service ?"
-> Consul répond : "http://192.168.1.10:8001" (dynamique)
-> Si l'IP change, Consul se met à jour automatiquement
```

**Code exemple (avec Consul):**

```python
# service_registry.py
import consul
import socket

class ServiceRegistry:
    def __init__(self, consul_host='localhost', consul_port=8500):
        self.consul = consul.Consul(host=consul_host, port=consul_port)
    
    def register_service(self, service_name, service_port):
        """Enregistrer un service dans Consul"""
        # Obtenir l'IP du serveur
        hostname = socket.gethostname()
        ip_address = socket.gethostbyname(hostname)
        
        # Enregistrer
        self.consul.agent.service.register(
            name=service_name,
            service_id=f"{service_name}-{service_port}",
            address=ip_address,
            port=service_port,
            check=consul.Check.http(
                url=f"http://{ip_address}:{service_port}/health",
                interval="10s",
                timeout="5s"
            )
        )
        
        print(f"Service {service_name} registered at {ip_address}:{service_port}")
    
    def discover_service(self, service_name):
        """Trouver un service dans Consul"""
        _, services = self.consul.health.service(service_name, passing=True)
        
        if not services:
            raise Exception(f"Service {service_name} not found")
        
        # Retourner le premier service disponible
        service = services[0]
        address = service['Service']['Address']
        port = service['Service']['Port']
        
        return f"http://{address}:{port}"

# Utilisation
registry = ServiceRegistry()

# Au démarrage du service User
registry.register_service('user-service', 8001)

# Dans le service Order, pour appeler User
user_service_url = registry.discover_service('user-service')
response = requests.get(f"{user_service_url}/users/123")
```

---

**3. Message Queue [EMAIL]**

```
Rôle : Communication asynchrone entre services

Synchrone (HTTP) :
Service A -> appelle -> Service B -> attend la réponse
[X] Si Service B est lent -> Service A est bloqué
[X] Si Service B est down -> Service A échoue

Asynchrone (Message Queue) :
Service A -> publie message -> Queue -> Service B consomme quand prêt
[OK] Service A n'attend pas
[OK] Service B peut être temporairement down
[OK] Messages garantis (retry automatique)
```

**Code exemple (RabbitMQ):**

```python
# publisher.py (Service Order)
import pika
import json

def publish_order_created(order_data):
    """Publier un événement 'order.created'"""
    
    # Connexion à RabbitMQ
    connection = pika.BlockingConnection(
        pika.ConnectionParameters('localhost')
    )
    channel = connection.channel()
    
    # Déclarer l'exchange
    channel.exchange_declare(
        exchange='orders',
        exchange_type='fanout'
    )
    
    # Publier le message
    message = json.dumps(order_data)
    channel.basic_publish(
        exchange='orders',
        routing_key='',
        body=message
    )
    
    print(f"Published: {message}")
    connection.close()

# consumer.py (Service Email)
import pika
import json

def consume_order_events():
    """Consommer les événements 'order.created'"""
    
    connection = pika.BlockingConnection(
        pika.ConnectionParameters('localhost')
    )
    channel = connection.channel()
    
    # Déclarer l'exchange
    channel.exchange_declare(
        exchange='orders',
        exchange_type='fanout'
    )
    
    # Créer une queue
    result = channel.queue_declare(queue='', exclusive=True)
    queue_name = result.method.queue
    
    # Lier la queue à l'exchange
    channel.queue_bind(
        exchange='orders',
        queue=queue_name
    )
    
    def callback(ch, method, properties, body):
        """Traiter un message"""
        order_data = json.loads(body)
        print(f"Received: {order_data}")
        
        # Envoyer l'email de confirmation
        send_order_confirmation_email(order_data)
    
    # Consommer
    channel.basic_consume(
        queue=queue_name,
        on_message_callback=callback,
        auto_ack=True
    )
    
    print("Waiting for order events...")
    channel.start_consuming()

# Utilisation
# Dans le service Order
order = create_order(user_id, items)
publish_order_created({
    'order_id': order.id,
    'user_id': order.user_id,
    'total': order.total
})
```

---

## [CODE] EXEMPLE COMPLET : E-COMMERCE EN MICROSERVICES

### Architecture

```
Services :
1. User Service (Port 8001) - Gestion utilisateurs
2. Product Service (Port 8002) - Catalogue produits
3. Order Service (Port 8003) - Commandes
4. Payment Service (Port 8004) - Paiements
5. Email Service (Port 8005) - Notifications
6. API Gateway (Port 8000) - Point d'entrée
```

---

### 1. User Service

```python
# services/user_service/app.py
from flask import Flask, jsonify, request
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://localhost/user_db'
db = SQLAlchemy(app)

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True)
    email = db.Column(db.String(120), unique=True)

@app.route('/health')
def health():
    return jsonify({'status': 'healthy'})

@app.route('/users/<int:user_id>')
def get_user(user_id):
    user = User.query.get_or_404(user_id)
    return jsonify({
        'id': user.id,
        'username': user.username,
        'email': user.email
    })

@app.route('/users', methods=['POST'])
def create_user():
    data = request.json
    user = User(
        username=data['username'],
        email=data['email']
    )
    db.session.add(user)
    db.session.commit()
    
    return jsonify({'id': user.id}), 201

if __name__ == '__main__':
    app.run(port=8001)
```

**Dockerfile:**

```dockerfile
FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install -r requirements.txt

COPY . .

CMD ["python", "app.py"]
```

---

### 2. Product Service

```python
# services/product_service/app.py
from flask import Flask, jsonify, request
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://localhost/product_db'
db = SQLAlchemy(app)

class Product(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(200))
    price = db.Column(db.Float)
    stock = db.Column(db.Integer, default=0)

@app.route('/health')
def health():
    return jsonify({'status': 'healthy'})

@app.route('/products')
def list_products():
    products = Product.query.all()
    return jsonify([{
        'id': p.id,
        'name': p.name,
        'price': p.price,
        'stock': p.stock
    } for p in products])

@app.route('/products/<int:product_id>')
def get_product(product_id):
    product = Product.query.get_or_404(product_id)
    return jsonify({
        'id': product.id,
        'name': product.name,
        'price': product.price,
        'stock': product.stock
    })

@app.route('/products/<int:product_id>/reduce-stock', methods=['POST'])
def reduce_stock(product_id):
    product = Product.query.get_or_404(product_id)
    quantity = request.json.get('quantity', 1)
    
    if product.stock < quantity:
        return jsonify({'error': 'Insufficient stock'}), 400
    
    product.stock -= quantity
    db.session.commit()
    
    return jsonify({'stock': product.stock})

if __name__ == '__main__':
    app.run(port=8002)
```

---

### 3. Order Service (avec communication inter-services)

```python
# services/order_service/app.py
from flask import Flask, jsonify, request
from flask_sqlalchemy import SQLAlchemy
import requests

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://localhost/order_db'
db = SQLAlchemy(app)

# URLs des autres services
USER_SERVICE = 'http://localhost:8001'
PRODUCT_SERVICE = 'http://localhost:8002'
PAYMENT_SERVICE = 'http://localhost:8004'
EMAIL_SERVICE = 'http://localhost:8005'

class Order(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer)
    product_id = db.Column(db.Integer)
    quantity = db.Column(db.Integer)
    total = db.Column(db.Float)
    status = db.Column(db.String(20), default='pending')

@app.route('/health')
def health():
    return jsonify({'status': 'healthy'})

@app.route('/orders', methods=['POST'])
def create_order():
    """
    Créer une commande (orchestration de plusieurs services)
    """
    data = request.json
    user_id = data['user_id']
    product_id = data['product_id']
    quantity = data.get('quantity', 1)
    
    try:
        # 1. Vérifier que l'utilisateur existe
        user_response = requests.get(f'{USER_SERVICE}/users/{user_id}', timeout=3)
        if user_response.status_code != 200:
            return jsonify({'error': 'User not found'}), 404
        user = user_response.json()
        
        # 2. Vérifier que le produit existe et a du stock
        product_response = requests.get(f'{PRODUCT_SERVICE}/products/{product_id}', timeout=3)
        if product_response.status_code != 200:
            return jsonify({'error': 'Product not found'}), 404
        product = product_response.json()
        
        if product['stock'] < quantity:
            return jsonify({'error': 'Insufficient stock'}), 400
        
        # 3. Calculer le total
        total = product['price'] * quantity
        
        # 4. Créer la commande en DB
        order = Order(
            user_id=user_id,
            product_id=product_id,
            quantity=quantity,
            total=total
        )
        db.session.add(order)
        db.session.commit()
        
        # 5. Réduire le stock
        stock_response = requests.post(
            f'{PRODUCT_SERVICE}/products/{product_id}/reduce-stock',
            json={'quantity': quantity},
            timeout=3
        )
        
        if stock_response.status_code != 200:
            # Rollback : Supprimer la commande
            db.session.delete(order)
            db.session.commit()
            return jsonify({'error': 'Failed to reduce stock'}), 500
        
        # 6. Traiter le paiement (asynchrone dans la vraie vie)
        try:
            payment_response = requests.post(
                f'{PAYMENT_SERVICE}/payments',
                json={
                    'order_id': order.id,
                    'user_id': user_id,
                    'amount': total
                },
                timeout=5
            )
            
            if payment_response.status_code == 200:
                order.status = 'paid'
                db.session.commit()
        except requests.exceptions.Timeout:
            # Paiement en timeout, mais commande créée
            pass
        
        # 7. Envoyer email de confirmation (asynchrone, non bloquant)
        try:
            requests.post(
                f'{EMAIL_SERVICE}/send',
                json={
                    'to': user['email'],
                    'subject': 'Order confirmation',
                    'body': f'Your order #{order.id} has been confirmed. Total: ${total}'
                },
                timeout=1  # Court timeout, pas critique
            )
        except:
            pass  # Email échoué, mais commande OK
        
        # 8. Retourner la commande créée
        return jsonify({
            'id': order.id,
            'user_id': order.user_id,
            'product_id': order.product_id,
            'quantity': order.quantity,
            'total': order.total,
            'status': order.status
        }), 201
        
    except requests.exceptions.Timeout:
        return jsonify({'error': 'Service timeout'}), 504
    except requests.exceptions.ConnectionError:
        return jsonify({'error': 'Service unavailable'}), 503

@app.route('/orders/<int:order_id>')
def get_order(order_id):
    order = Order.query.get_or_404(order_id)
    return jsonify({
        'id': order.id,
        'user_id': order.user_id,
        'product_id': order.product_id,
        'total': order.total,
        'status': order.status
    })

if __name__ == '__main__':
    app.run(port=8003)
```

---

### 4. Docker Compose (orchestration)

```yaml
# docker-compose.yml
version: '3.8'

services:
  # Bases de données
  user-db:
    image: postgres:15
    environment:
      POSTGRES_DB: user_db
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
    ports:
      - "5432:5432"
  
  product-db:
    image: postgres:15
    environment:
      POSTGRES_DB: product_db
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
    ports:
      - "5433:5432"
  
  order-db:
    image: postgres:15
    environment:
      POSTGRES_DB: order_db
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
    ports:
      - "5434:5432"
  
  # Services
  user-service:
    build: ./services/user_service
    ports:
      - "8001:8001"
    depends_on:
      - user-db
    environment:
      DATABASE_URL: postgresql://user:password@user-db:5432/user_db
  
  product-service:
    build: ./services/product_service
    ports:
      - "8002:8002"
    depends_on:
      - product-db
    environment:
      DATABASE_URL: postgresql://user:password@product-db:5432/product_db
  
  order-service:
    build: ./services/order_service
    ports:
      - "8003:8003"
    depends_on:
      - order-db
      - user-service
      - product-service
    environment:
      DATABASE_URL: postgresql://user:password@order-db:5432/order_db
      USER_SERVICE_URL: http://user-service:8001
      PRODUCT_SERVICE_URL: http://product-service:8002
  
  payment-service:
    build: ./services/payment_service
    ports:
      - "8004:8004"
  
  email-service:
    build: ./services/email_service
    ports:
      - "8005:8005"
  
  # API Gateway
  api-gateway:
    build: ./api_gateway
    ports:
      - "8000:8000"
    depends_on:
      - user-service
      - product-service
      - order-service
```

**Démarrage:**

```bash
# Build et démarrer tous les services
docker-compose up --build

# Tester
curl http://localhost:8000/users/1
curl http://localhost:8000/products
curl -X POST http://localhost:8000/orders \
  -H "Content-Type: application/json" \
  -d '{"user_id": 1, "product_id": 1, "quantity": 2}'
```

---

## [OBJECTIF] PATTERNS MICROSERVICES IMPORTANTS

### 1. Circuit Breaker Pattern

```python
# circuit_breaker.py
import time
from enum import Enum

class CircuitState(Enum):
    CLOSED = "closed"     # Tout fonctionne
    OPEN = "open"         # Service down, requêtes bloquées
    HALF_OPEN = "half_open"  # Test si service de retour

class CircuitBreaker:
    def __init__(self, failure_threshold=5, timeout=60):
        self.failure_threshold = failure_threshold
        self.timeout = timeout
        self.failure_count = 0
        self.last_failure_time = None
        self.state = CircuitState.CLOSED
    
    def call(self, func, *args, **kwargs):
        """Exécuter une fonction avec circuit breaker"""
        
        # Si circuit ouvert
        if self.state == CircuitState.OPEN:
            # Vérifier si timeout écoulé
            if time.time() - self.last_failure_time > self.timeout:
                self.state = CircuitState.HALF_OPEN
            else:
                raise Exception("Circuit breaker is OPEN")
        
        try:
            # Essayer d'exécuter
            result = func(*args, **kwargs)
            
            # Succès : Réinitialiser
            self.failure_count = 0
            self.state = CircuitState.CLOSED
            
            return result
            
        except Exception as e:
            # Échec : Incrémenter compteur
            self.failure_count += 1
            self.last_failure_time = time.time()
            
            # Si seuil atteint : Ouvrir le circuit
            if self.failure_count >= self.failure_threshold:
                self.state = CircuitState.OPEN
            
            raise e

# Utilisation
payment_circuit = CircuitBreaker(failure_threshold=3, timeout=30)

def process_payment(amount):
    try:
        return payment_circuit.call(
            requests.post,
            'http://payment-service/pay',
            json={'amount': amount},
            timeout=3
        )
    except Exception as e:
        # Fallback : Mettre en queue
        queue.add_payment(amount)
        return {'status': 'queued'}
```

---

### 2. API Composition Pattern

```python
# api_composition.py
import asyncio
import aiohttp

async def get_user_with_orders(user_id):
    """
    Composer les données de plusieurs services
    """
    async with aiohttp.ClientSession() as session:
        # Appels parallèles
        user_task = fetch_user(session, user_id)
        orders_task = fetch_user_orders(session, user_id)
        
        # Attendre les résultats
        user, orders = await asyncio.gather(user_task, orders_task)
        
        # Composer la réponse
        return {
            'user': user,
            'orders': orders,
            'total_spent': sum(order['total'] for order in orders)
        }

async def fetch_user(session, user_id):
    async with session.get(f'http://user-service/users/{user_id}') as response:
        return await response.json()

async def fetch_user_orders(session, user_id):
    async with session.get(f'http://order-service/users/{user_id}/orders') as response:
        return await response.json()

# API Endpoint
@app.route('/users/<int:user_id>/full')
async def get_user_full(user_id):
    data = await get_user_with_orders(user_id)
    return jsonify(data)
```

---

## [COURS] QUAND UTILISER LES MICROSERVICES ?

### [OK] Utilise les microservices SI :

```
1. Application LARGE et COMPLEXE
   - > 100,000 lignes de code
   - > 10 fonctionnalités majeures
   - Croissance continue prévue

2. Équipe GRANDE
   - > 10 développeurs
   - Plusieurs équipes
   - Besoin d'autonomie des équipes

3. Scalabilité DIFFÉRENCIÉE nécessaire
   - Certaines parties ont beaucoup plus de charge
   - Besoin de scaler finement

4. Technologies DIVERSES requises
   - Certains services nécessitent des technos spécifiques
   - Machine Learning + Temps réel + APIs classiques

5. Déploiement CONTINU critique
   - Déploiements multiples par jour
   - Zero-downtime obligatoire

6. LONG TERME (> 2 ans)
   - Investissement initial rentabilisé
   - Maintenance facilitée sur le long terme
```

---

### [X] N'utilise PAS les microservices SI :

```
1. Projet PETIT ou PROTOTYPE
   - < 50,000 lignes de code
   - MVP/POC
   - Validation d'idée

2. Équipe PETITE
   - < 5 développeurs
   - Pas d'expertise DevOps
   - Budget limité

3. Scalabilité UNIFORME
   - Toute l'app a la même charge
   - Scalabilité horizontale simple suffit

4. Time-to-market CRITIQUE
   - Besoin de lancer rapidement
   - Ressources limitées

5. Complexité NON JUSTIFIÉE
   - Application simple
   - Peu de fonctionnalités

COMMENCE PAR UN MONOLITHE
puis migre vers microservices SI NÉCESSAIRE
```

---

## [BRAVO] CONCLUSION

### Ce que tu as appris

**Architecture Microservices :**
- [OK] Définition et principes
- [OK] Communication inter-services
- [OK] API Gateway, Service Discovery
- [OK] Message Queue, Circuit Breaker
- [OK] Avantages et inconvénients détaillés

**Pratique :**
- [OK] Exemple e-commerce complet
- [OK] Docker Compose pour orchestration
- [OK] Patterns essentiels
- [OK] Quand utiliser vs éviter

---

### Points clés

```
[OBJECTIF] Microservices = Services indépendants
[OBJECTIF] Chaque service = Une fonction métier
[OBJECTIF] Communication via réseau (HTTP/Messages)
[OBJECTIF] Scalabilité fine et ciblée
[OBJECTIF] Complexité opérationnelle élevée
[OBJECTIF] Idéal pour grandes applications
[OBJECTIF] Commence simple, évolue si nécessaire
```

---

### Parcours d'évolution recommandé

```
ÉTAPE 1 : Monolithe (0-6 mois)
   -> Validation de l'idée
   -> Développement rapide
   v

ÉTAPE 2 : Monolithe modulaire (6-12 mois)
   -> Structure en modules clairs
   -> Préparation à la séparation
   v

ÉTAPE 3 : Extraction progressive (12-24 mois)
   -> 1-2 services à la fois
   -> Services périphériques d'abord
   v

ÉTAPE 4 : Architecture hybride (24+ mois)
   -> Monolithe core + Microservices
   -> Équilibre optimal
   v

ÉTAPE 5 (optionnel) : Full microservices
   -> Si vraiment nécessaire
   -> Expertise DevOps mature
```

---

### Prochaines étapes

1. Code les exercices
2. Lis `architecture_hexagonale.txt`
3. Lis `architecture_event_driven.txt`
4. Compare toutes les architectures

**[RAPIDE] Tu maîtrises maintenant les Microservices ! [RAPIDE]**

═══════════════════════════════════════════════════════════════
FIN DU FICHIER : architecture_microservices.txt
═══════════════════════════════════════════════════════════════

# [ENTREPRISE] ARCHITECTURE EN COUCHES (LAYERED / N-TIER) - GUIDE ULTRA DÉTAILLÉ

## * INTRODUCTION

### Qu'est-ce que l'architecture en couches ?

**Architecture en couches (Layered Architecture)** = Organisation de l'application en **couches horizontales**, où chaque couche a une **responsabilité spécifique** et ne communique qu'avec les couches adjacentes.

**Analogie : Gâteau à étages [BIRTHDAY_CAKE]**

```
[BIRTHDAY_CAKE] GÂTEAU À ÉTAGES (Architecture en couches)

Couche 4 : [STRAWBERRY] Décoration      (Présentation - UI)
           ─────────────
Couche 3 : [SHORTCAKE] Crème           (Logique métier)
           ─────────────
Couche 2 : [COOKIE] Biscuit         (Accès aux données)
           ─────────────
Couche 1 : [PACKAGE] Support         (Base de données)

Règles :
[OK] Chaque couche repose sur la couche du dessous
[OK] Les couches ne sautent PAS d'étages
[OK] Communication descendante uniquement (ou adjacente)
[X] La crème ne parle PAS directement au support
```

**En informatique :**

```
┌─────────────────────────────────────────────┐
│  COUCHE PRÉSENTATION (Presentation Layer)  │  <- Interface utilisateur
│  - Web UI, Mobile UI, Desktop UI, API      │
└──────────────┬──────────────────────────────┘
               v (appelle uniquement)
┌─────────────────────────────────────────────┐
│  COUCHE MÉTIER (Business Logic Layer)      │  <- Logique applicative
│  - Règles métier, Validations, Workflows   │
└──────────────┬──────────────────────────────┘
               v (appelle uniquement)
┌─────────────────────────────────────────────┐
│  COUCHE PERSISTANCE (Data Access Layer)    │  <- Accès données
│  - Repositories, DAO, ORM                   │
└──────────────┬──────────────────────────────┘
               v (appelle uniquement)
┌─────────────────────────────────────────────┐
│  COUCHE DONNÉES (Database Layer)           │  <- Stockage
│  - PostgreSQL, MySQL, MongoDB, etc.        │
└─────────────────────────────────────────────┘

RÈGLE D'OR : Les dépendances vont TOUJOURS vers le bas
            (Présentation -> Métier -> Persistance -> Données)
```

---

### [GRAPHIQUE] Variantes d'architecture en couches

**1. Architecture 3-tiers (3-tier) :**

```
Tier 1 : CLIENT (Frontend)
         - Navigateur web
         - Application mobile
         - Application desktop
         v
Tier 2 : SERVEUR APPLICATION (Backend)
         - Logique métier
         - API REST/GraphQL
         v
Tier 3 : SERVEUR BASE DE DONNÉES
         - PostgreSQL, MySQL, etc.

Exemple : Application web classique
Client (React) -> API (Node.js) -> Base de données (PostgreSQL)
```

---

**2. Architecture 4-couches (4-layer) :**

```
Layer 1 : PRÉSENTATION
          - Controllers, Views, API Endpoints
          v
Layer 2 : SERVICES / LOGIQUE MÉTIER
          - Business Services, Use Cases
          v
Layer 3 : DATA ACCESS (Persistance)
          - Repositories, DAOs, ORM
          v
Layer 4 : BASE DE DONNÉES
          - SGBD, Fichiers, Cache
```

---

**3. Architecture 5-couches (5-layer) :**

```
Layer 1 : PRÉSENTATION
          - UI, Controllers
          v
Layer 2 : SERVICE / APPLICATION
          - Application Services, DTOs
          v
Layer 3 : DOMAIN / MÉTIER
          - Entités métier, Règles business
          v
Layer 4 : DATA ACCESS
          - Repositories, Mappers
          v
Layer 5 : BASE DE DONNÉES
          - SGBD
```

---

## [REFLEXION] POURQUOI UTILISER L'ARCHITECTURE EN COUCHES ?

### [OK] Avantages

**1. Séparation des préoccupations (Separation of Concerns) [OBJECTIF]**

**Sans couches (tout mélangé) :**

```python
# [X] Tout dans une seule fonction - CODE SPAGHETTI
@app.route('/users/<user_id>/orders')
def get_user_orders(user_id):
    # HTML mélangé au code
    html = "<html><head><title>Orders</title></head><body>"
    
    # Requête SQL directe
    conn = psycopg2.connect("dbname=myapp user=postgres")
    cursor = conn.cursor()
    cursor.execute("""
        SELECT o.id, o.total, o.date, p.name, p.price
        FROM orders o
        JOIN order_items oi ON o.id = oi.order_id
        JOIN products p ON oi.product_id = p.id
        WHERE o.user_id = %s
    """, (user_id,))
    
    orders = cursor.fetchall()
    
    # Logique métier mélangée
    total_spent = 0
    for order in orders:
        total_spent += order[1]
        if order[1] > 100:
            discount = order[1] * 0.1  # 10% de réduction
        else:
            discount = 0
        
        html += f"<div>Order #{order[0]}: ${order[1] - discount}</div>"
    
    html += f"<p>Total dépensé: ${total_spent}</p>"
    html += "</body></html>"
    
    conn.close()
    return html

# Problèmes :
# [X] HTML, SQL, et logique business mélangés
# [X] Impossible de tester la logique séparément
# [X] Impossible de réutiliser la logique
# [X] Modification du HTML = risque de casser la logique
# [X] Changement de DB = tout réécrire
```

---

**Avec architecture en couches (séparé) :**

```python
# [OK] COUCHE 1 : PRÉSENTATION (Controller)
@app.route('/users/<user_id>/orders')
def get_user_orders(user_id):
    """Controller - Gère uniquement les requêtes HTTP"""
    
    # Appeler le service métier
    order_service = OrderService()
    orders = order_service.get_user_orders(user_id)
    total_spent = order_service.calculate_total_spent(user_id)
    
    # Retourner la vue
    return render_template('orders.html', 
                         orders=orders, 
                         total_spent=total_spent)

# [OK] COUCHE 2 : LOGIQUE MÉTIER (Service)
class OrderService:
    """Service - Contient la logique métier"""
    
    def __init__(self):
        self.order_repository = OrderRepository()
    
    def get_user_orders(self, user_id):
        """Récupère les commandes avec logique métier appliquée"""
        # Récupérer les données
        orders = self.order_repository.find_by_user_id(user_id)
        
        # Appliquer la logique métier (discount)
        for order in orders:
            if order.total > 100:
                order.discount = order.total * 0.1
            else:
                order.discount = 0
            order.final_total = order.total - order.discount
        
        return orders
    
    def calculate_total_spent(self, user_id):
        """Calcule le total dépensé"""
        orders = self.order_repository.find_by_user_id(user_id)
        return sum(order.total for order in orders)

# [OK] COUCHE 3 : ACCÈS AUX DONNÉES (Repository)
class OrderRepository:
    """Repository - Gère uniquement l'accès aux données"""
    
    def __init__(self):
        self.db = Database()
    
    def find_by_user_id(self, user_id):
        """Récupère les commandes depuis la DB"""
        query = """
            SELECT o.id, o.total, o.date, o.status
            FROM orders o
            WHERE o.user_id = %s
        """
        rows = self.db.execute(query, (user_id,))
        
        # Convertir en objets
        return [Order.from_db_row(row) for row in rows]

# [OK] COUCHE 4 : BASE DE DONNÉES (Modèle)
class Order:
    """Entité - Représente une commande"""
    
    def __init__(self, id, total, date, status):
        self.id = id
        self.total = total
        self.date = date
        self.status = status
        self.discount = 0
        self.final_total = total
    
    @classmethod
    def from_db_row(cls, row):
        """Crée un Order depuis une ligne DB"""
        return cls(row[0], row[1], row[2], row[3])

# Avantages :
# [OK] Chaque couche a une responsabilité claire
# [OK] Facile de tester chaque couche séparément
# [OK] Modifier le HTML ne touche pas la logique
# [OK] Changer de DB = modifier uniquement le Repository
# [OK] Réutilisable (le Service peut être appelé par API, CLI, etc.)
```

---

**2. Maintenabilité améliorée [OUTIL]**

**Scénario : Ajouter une fonctionnalité de cache**

**Sans couches :**

```python
# [X] Modifier TOUTES les fonctions qui accèdent aux données
def get_user(user_id):
    # Ajouter cache ici
    cached = cache.get(f'user:{user_id}')
    if cached:
        return cached
    
    user = db.query(...)
    cache.set(f'user:{user_id}', user)
    return user

def get_orders(user_id):
    # Répéter le même code de cache ici
    cached = cache.get(f'orders:{user_id}')
    if cached:
        return cached
    
    orders = db.query(...)
    cache.set(f'orders:{user_id}', orders)
    return orders

# ... Répéter pour 50+ fonctions [!]
```

---

**Avec couches :**

```python
# [OK] Modifier UNIQUEMENT la couche Data Access
class CachedOrderRepository(OrderRepository):
    """Repository avec cache - Décorateur"""
    
    def __init__(self, base_repository, cache):
        self.base_repository = base_repository
        self.cache = cache
    
    def find_by_user_id(self, user_id):
        # Vérifier le cache
        cache_key = f'orders:user:{user_id}'
        cached = self.cache.get(cache_key)
        
        if cached:
            return cached
        
        # Sinon, déléguer au repository de base
        orders = self.base_repository.find_by_user_id(user_id)
        
        # Mettre en cache
        self.cache.set(cache_key, orders, expire=300)
        
        return orders

# Utilisation (une seule ligne à changer)
# Avant :
order_repository = OrderRepository()

# Après (avec cache) :
order_repository = CachedOrderRepository(OrderRepository(), redis_cache)

# [OK] Toutes les couches supérieures continuent de fonctionner
# [OK] Une seule classe modifiée
# [OK] Facile à tester
# [OK] Facile à retirer le cache si besoin
```

---

**3. Réutilisabilité [SYNC]**

**La même logique métier utilisée par plusieurs interfaces :**

```python
# COUCHE MÉTIER (réutilisable)
class UserService:
    def authenticate(self, email, password):
        """Logique d'authentification"""
        user = self.user_repository.find_by_email(email)
        
        if not user:
            raise InvalidCredentials()
        
        if not user.check_password(password):
            raise InvalidCredentials()
        
        if not user.is_active:
            raise AccountDisabled()
        
        return user

# INTERFACE WEB (utilise le service)
@app.route('/login', methods=['POST'])
def web_login():
    email = request.form['email']
    password = request.form['password']
    
    try:
        user = user_service.authenticate(email, password)
        session['user_id'] = user.id
        return redirect('/dashboard')
    except InvalidCredentials:
        return render_template('login.html', error="Invalid credentials")

# API REST (utilise le même service)
@api.route('/api/auth/login', methods=['POST'])
def api_login():
    data = request.json
    
    try:
        user = user_service.authenticate(data['email'], data['password'])
        token = generate_jwt_token(user.id)
        return jsonify({'token': token})
    except InvalidCredentials:
        return jsonify({'error': 'Invalid credentials'}), 401

# CLI (utilise le même service)
def cli_login(email, password):
    try:
        user = user_service.authenticate(email, password)
        print(f"Welcome {user.username}!")
    except InvalidCredentials:
        print("Invalid credentials")

# [OK] Même logique, 3 interfaces différentes
# [OK] Pas de duplication de code
# [OK] Modification de la logique = une seule fois
```

---

**4. Testabilité [TEST]**

**Tests isolés par couche :**

```python
# TEST COUCHE MÉTIER (avec mock du repository)
def test_authenticate_success():
    # Arrange : Mock du repository
    mock_repo = Mock()
    mock_user = User(id=1, email='test@example.com', is_active=True)
    mock_user.password_hash = User.hash_password('password123')
    mock_repo.find_by_email.return_value = mock_user
    
    # Service avec mock
    service = UserService(mock_repo)
    
    # Act
    user = service.authenticate('test@example.com', 'password123')
    
    # Assert
    assert user.id == 1
    mock_repo.find_by_email.assert_called_once_with('test@example.com')

def test_authenticate_invalid_password():
    # Arrange
    mock_repo = Mock()
    mock_user = User(id=1, email='test@example.com')
    mock_user.password_hash = User.hash_password('correct_password')
    mock_repo.find_by_email.return_value = mock_user
    
    service = UserService(mock_repo)
    
    # Act & Assert
    with pytest.raises(InvalidCredentials):
        service.authenticate('test@example.com', 'wrong_password')

# TEST COUCHE REPOSITORY (avec DB de test)
def test_repository_find_by_email():
    # Arrange : DB de test en mémoire
    test_db = create_test_database()
    repository = UserRepository(test_db)
    
    # Créer un utilisateur de test
    test_db.execute("INSERT INTO users (email) VALUES ('test@example.com')")
    
    # Act
    user = repository.find_by_email('test@example.com')
    
    # Assert
    assert user is not None
    assert user.email == 'test@example.com'

# TEST COUCHE PRÉSENTATION (avec mock du service)
def test_web_login_success():
    # Arrange
    mock_service = Mock()
    mock_service.authenticate.return_value = User(id=1, username='john')
    
    app.user_service = mock_service
    client = app.test_client()
    
    # Act
    response = client.post('/login', data={
        'email': 'test@example.com',
        'password': 'password123'
    })
    
    # Assert
    assert response.status_code == 302  # Redirect
    assert '/dashboard' in response.location

# [OK] Chaque couche testée indépendamment
# [OK] Tests rapides (pas de DB pour les services)
# [OK] Tests isolés (pas de dépendances externes)
```

---

**5. Facilite le travail en équipe [UTILISATEURS]**

```
Équipe Backend (3 développeurs) :

Dev 1 : Couche Présentation (Controllers, API)
        - Routes HTTP
        - Validation des entrées
        - Formatage des réponses

Dev 2 : Couche Métier (Services)
        - Logique business
        - Règles métier
        - Orchestration

Dev 3 : Couche Data Access (Repositories)
        - Requêtes SQL
        - Optimisations DB
        - Migrations

Travail parallèle sans conflits :
[OK] Chacun travaille sur sa couche
[OK] Interface claire entre couches (contrats)
[OK] Intégration facile
[OK] Pas de marche sur les pieds
```

---

### [X] Inconvénients

**1. Performance (latence des couches) [TEMPS]**

**Chaque couche ajoute de la latence :**

```python
# Requête simple : Récupérer un utilisateur

# Trajet complet :
Controller (0.1 ms)
    v
Service (0.1 ms)
    v
Repository (0.2 ms)
    v
Database (10 ms)
    v
Repository (0.2 ms)
    v
Service (0.1 ms)
    v
Controller (0.1 ms)

Total : 10.8 ms

# Si accès direct (sans couches) :
Controller -> Database -> Controller
Total : 10.2 ms

Overhead : 0.6 ms (6% plus lent)

# Pour 1000 requêtes/sec : 600 ms perdues
# Peut devenir significatif à très haute échelle
```

**Solution :** Caching agressif, optimisations ciblées

---

**2. Over-engineering pour petits projets [MESURE]**

**Pour un projet très simple :**

```python
# Sans couches (simple et suffisant)
# 1 fichier, 20 lignes
@app.route('/hello/<name>')
def hello(name):
    return f"Hello {name}!"

# Avec couches (overkill)
# 10 fichiers, 100+ lignes

# controllers/hello_controller.py
def hello(name):
    service = HelloService()
    return service.greet(name)

# services/hello_service.py
class HelloService:
    def greet(self, name):
        return f"Hello {name}!"

# [!] Complexité inutile pour "Hello World"
```

**Règle :** N'utilise les couches QUE si le projet le justifie

---

**3. Rigidité (difficulté à "sauter" des couches) [VERROUILLE]**

**Problème : Besoin d'accéder directement à la DB depuis le Controller**

```python
# Cas d'usage : Export massif de données en CSV

# Architecture stricte (forcé de passer par toutes les couches)
@app.route('/export-users-csv')
def export_users():
    # Controller -> Service
    user_service = UserService()
    users = user_service.get_all_users()  # Charge TOUT en mémoire
    
    # Service -> Repository
    # Repository -> DB
    
    # Générer CSV
    csv = generate_csv(users)  # 1 million d'utilisateurs = OOM [IMPACT]
    return csv

# Solution directe (souhaitable mais "interdit" par l'archi)
@app.route('/export-users-csv')
def export_users():
    # Accès direct à la DB pour streaming
    cursor = db.execute("SELECT * FROM users")
    
    def generate():
        for row in cursor:
            yield format_csv_row(row)
    
    return Response(generate(), mimetype='text/csv')
    # [OK] Streaming, pas de OOM

# Problème : L'architecture stricte empêche l'optimisation
```

**Solution :** Architecture "ouverte" (Open Layered Architecture)

---

**4. Boilerplate code [NOTE]**

**Beaucoup de code "passeur" (pass-through) :**

```python
# Controller (juste appelle le service)
@app.route('/users/<user_id>')
def get_user(user_id):
    user = user_service.get_user(user_id)  # <- Juste passer au service
    return jsonify(user.to_dict())

# Service (juste appelle le repository)
class UserService:
    def get_user(self, user_id):
        return self.user_repository.find_by_id(user_id)  # <- Juste passer au repo

# Repository (juste appelle la DB)
class UserRepository:
    def find_by_id(self, user_id):
        return self.db.query(User).filter_by(id=user_id).first()  # <- Juste passer à la DB

# 3 couches pour faire... un simple SELECT [NEUTRAL_FACE]

# Sans couches (direct) :
@app.route('/users/<user_id>')
def get_user(user_id):
    return jsonify(User.query.get(user_id).to_dict())

# 1 ligne VS 3 classes... Pour un cas simple
```

**Quand accepter le boilerplate :**
- Application grande et complexe
- Besoin d'évolution future
- Logique métier sera ajoutée plus tard

**Quand éviter :**
- Petites applications
- CRUD simple
- Pas d'évolution prévue

---

## [CONSTRUCTION] ARCHITECTURE COMPLÈTE EN COUCHES

### [DOSSIER] Structure de projet (4-couches)

```
layered_app/
│
├── presentation/                    # COUCHE 1 : PRÉSENTATION
│   ├── __init__.py
│   ├── controllers/                 # Controllers (Web)
│   │   ├── __init__.py
│   │   ├── user_controller.py
│   │   ├── product_controller.py
│   │   └── order_controller.py
│   │
│   ├── api/                         # API REST
│   │   ├── __init__.py
│   │   ├── user_api.py
│   │   └── product_api.py
│   │
│   ├── cli/                         # Interface CLI
│   │   ├── __init__.py
│   │   └── commands.py
│   │
│   ├── dto/                         # Data Transfer Objects
│   │   ├── __init__.py
│   │   ├── user_dto.py
│   │   └── product_dto.py
│   │
│   └── validators/                  # Validation des entrées
│       ├── __init__.py
│       └── request_validators.py
│
├── business/                        # COUCHE 2 : LOGIQUE MÉTIER
│   ├── __init__.py
│   ├── services/                    # Services métier
│   │   ├── __init__.py
│   │   ├── user_service.py
│   │   ├── product_service.py
│   │   ├── order_service.py
│   │   └── payment_service.py
│   │
│   ├── domain/                      # Entités métier
│   │   ├── __init__.py
│   │   ├── user.py
│   │   ├── product.py
│   │   └── order.py
│   │
│   ├── exceptions/                  # Exceptions métier
│   │   ├── __init__.py
│   │   └── business_exceptions.py
│   │
│   └── rules/                       # Règles métier
│       ├── __init__.py
│       ├── pricing_rules.py
│       └── discount_rules.py
│
├── data_access/                     # COUCHE 3 : ACCÈS AUX DONNÉES
│   ├── __init__.py
│   ├── repositories/                # Repositories
│   │   ├── __init__.py
│   │   ├── user_repository.py
│   │   ├── product_repository.py
│   │   └── order_repository.py
│   │
│   ├── models/                      # Modèles ORM
│   │   ├── __init__.py
│   │   ├── user_model.py
│   │   ├── product_model.py
│   │   └── order_model.py
│   │
│   ├── mappers/                     # Entity <-> Model mappers
│   │   ├── __init__.py
│   │   └── user_mapper.py
│   │
│   └── database.py                  # Configuration DB
│
├── infrastructure/                  # COUCHE 4 : INFRASTRUCTURE
│   ├── __init__.py
│   ├── cache/                       # Système de cache
│   │   ├── __init__.py
│   │   └── redis_cache.py
│   │
│   ├── messaging/                   # Files de messages
│   │   ├── __init__.py
│   │   └── rabbitmq.py
│   │
│   ├── email/                       # Envoi d'emails
│   │   ├── __init__.py
│   │   └── smtp_client.py
│   │
│   └── storage/                     # Stockage fichiers
│       ├── __init__.py
│       └── s3_storage.py
│
├── tests/                           # Tests
│   ├── unit/                        # Tests unitaires
│   │   ├── business/
│   │   └── data_access/
│   │
│   ├── integration/                 # Tests d'intégration
│   └── e2e/                         # Tests end-to-end
│
├── config/                          # Configuration
│   ├── __init__.py
│   ├── settings.py
│   └── logging_config.py
│
├── app.py                           # Point d'entrée
├── requirements.txt
└── README.md
```

---

## [CODE] CODE COMPLET - EXEMPLE E-COMMERCE

### COUCHE 1 : PRÉSENTATION

**presentation/controllers/order_controller.py**

```python
from flask import Blueprint, request, jsonify, render_template
from business.services.order_service import OrderService
from presentation.dto.order_dto import CreateOrderDTO, OrderResponseDTO
from presentation.validators.request_validators import validate_create_order

order_bp = Blueprint('orders', __name__, url_prefix='/orders')

class OrderController:
    """
    Controller - Couche Présentation
    
    Responsabilités :
    - Gérer les requêtes HTTP
    - Valider les entrées
    - Appeler les services métier
    - Formater les réponses
    
    NE DOIT PAS :
    - Contenir de logique métier
    - Accéder directement à la base de données
    """
    
    def __init__(self, order_service: OrderService):
        self.order_service = order_service
    
    def get_order(self, order_id: int):
        """
        GET /orders/<order_id>
        Récupérer une commande
        """
        try:
            # Appeler le service
            order = self.order_service.get_order(order_id)
            
            # Convertir en DTO pour la réponse
            response_dto = OrderResponseDTO.from_entity(order)
            
            return jsonify(response_dto.to_dict()), 200
            
        except OrderNotFoundError:
            return jsonify({'error': 'Order not found'}), 404
        except Exception as e:
            return jsonify({'error': 'Internal server error'}), 500
    
    def create_order(self):
        """
        POST /orders
        Créer une commande
        """
        try:
            # Récupérer les données de la requête
            data = request.get_json()
            
            # Valider les données (validation basique)
            validation_error = validate_create_order(data)
            if validation_error:
                return jsonify({'error': validation_error}), 400
            
            # Convertir en DTO
            create_dto = CreateOrderDTO.from_dict(data)
            
            # Appeler le service métier
            order = self.order_service.create_order(
                user_id=create_dto.user_id,
                items=create_dto.items
            )
            
            # Convertir la réponse
            response_dto = OrderResponseDTO.from_entity(order)
            
            return jsonify(response_dto.to_dict()), 201
            
        except InsufficientStockError as e:
            return jsonify({'error': str(e)}), 400
        except UserNotFoundError:
            return jsonify({'error': 'User not found'}), 404
        except Exception as e:
            # Log l'erreur
            app.logger.error(f"Error creating order: {e}")
            return jsonify({'error': 'Internal server error'}), 500
    
    def list_user_orders(self, user_id: int):
        """
        GET /users/<user_id>/orders
        Liste des commandes d'un utilisateur
        """
        try:
            # Pagination
            page = request.args.get('page', 1, type=int)
            per_page = request.args.get('per_page', 10, type=int)
            
            # Appeler le service
            orders, total = self.order_service.get_user_orders(
                user_id=user_id,
                page=page,
                per_page=per_page
            )
            
            # Convertir en DTOs
            orders_dto = [OrderResponseDTO.from_entity(order) for order in orders]
            
            return jsonify({
                'orders': [dto.to_dict() for dto in orders_dto],
                'page': page,
                'per_page': per_page,
                'total': total
            }), 200
            
        except Exception as e:
            return jsonify({'error': 'Internal server error'}), 500

# Routes
order_controller = OrderController(order_service)

@order_bp.route('/<int:order_id>', methods=['GET'])
def get_order(order_id):
    return order_controller.get_order(order_id)

@order_bp.route('/', methods=['POST'])
def create_order():
    return order_controller.create_order()

@order_bp.route('/users/<int:user_id>/orders', methods=['GET'])
def list_user_orders(user_id):
    return order_controller.list_user_orders(user_id)
```

**presentation/dto/order_dto.py**

```python
from dataclasses import dataclass
from typing import List
from datetime import datetime

@dataclass
class OrderItemDTO:
    """DTO pour un item de commande"""
    product_id: int
    quantity: int
    price: float = None  # Sera rempli par le service

@dataclass
class CreateOrderDTO:
    """DTO pour créer une commande (entrée)"""
    user_id: int
    items: List[OrderItemDTO]
    
    @classmethod
    def from_dict(cls, data: dict):
        """Créer depuis un dictionnaire"""
        items = [
            OrderItemDTO(
                product_id=item['product_id'],
                quantity=item['quantity']
            )
            for item in data.get('items', [])
        ]
        
        return cls(
            user_id=data['user_id'],
            items=items
        )

@dataclass
class OrderResponseDTO:
    """DTO pour une commande (sortie)"""
    id: int
    user_id: int
    items: List[dict]
    total: float
    status: str
    created_at: str
    
    @classmethod
    def from_entity(cls, order):
        """Créer depuis une entité métier"""
        return cls(
            id=order.id,
            user_id=order.user_id,
            items=[
                {
                    'product_id': item.product_id,
                    'product_name': item.product_name,
                    'quantity': item.quantity,
                    'price': item.price
                }
                for item in order.items
            ],
            total=order.calculate_total(),
            status=order.status,
            created_at=order.created_at.isoformat()
        )
    
    def to_dict(self):
        """Convertir en dictionnaire"""
        return {
            'id': self.id,
            'user_id': self.user_id,
            'items': self.items,
            'total': self.total,
            'status': self.status,
            'created_at': self.created_at
        }
```

---

### COUCHE 2 : LOGIQUE MÉTIER

**business/services/order_service.py**

```python
from typing import List, Tuple
from business.domain.order import Order, OrderItem
from business.exceptions.business_exceptions import *
from data_access.repositories.order_repository import OrderRepository
from data_access.repositories.product_repository import ProductRepository
from data_access.repositories.user_repository import UserRepository

class OrderService:
    """
    Service métier - Couche Business
    
    Responsabilités :
    - Logique métier
    - Règles business
    - Validation des données business
    - Orchestration
    
    NE DOIT PAS :
    - Gérer les requêtes HTTP
    - Accéder directement à la base de données (utilise repositories)
    """
    
    def __init__(
        self,
        order_repository: OrderRepository,
        product_repository: ProductRepository,
        user_repository: UserRepository
    ):
        self.order_repository = order_repository
        self.product_repository = product_repository
        self.user_repository = user_repository
    
    def create_order(self, user_id: int, items: List[dict]) -> Order:
        """
        Créer une commande
        
        Logique métier :
        1. Vérifier que l'utilisateur existe
        2. Vérifier que tous les produits existent et ont du stock
        3. Appliquer les règles de pricing
        4. Créer la commande
        5. Réduire les stocks
        """
        
        # 1. Vérifier l'utilisateur
        user = self.user_repository.find_by_id(user_id)
        if not user:
            raise UserNotFoundError(f"User {user_id} not found")
        
        # 2. Valider et préparer les items
        order_items = []
        for item_data in items:
            product_id = item_data['product_id']
            quantity = item_data['quantity']
            
            # Récupérer le produit
            product = self.product_repository.find_by_id(product_id)
            if not product:
                raise ProductNotFoundError(f"Product {product_id} not found")
            
            # Vérifier le stock
            if product.stock < quantity:
                raise InsufficientStockError(
                    f"Insufficient stock for product {product.name}. "
                    f"Available: {product.stock}, Requested: {quantity}"
                )
            
            # Créer l'OrderItem avec le prix actuel
            order_item = OrderItem(
                product_id=product.id,
                product_name=product.name,
                price=product.price,
                quantity=quantity
            )
            order_items.append(order_item)
        
        # 3. Créer l'entité Order
        order = Order(
            user_id=user_id,
            items=order_items
        )
        
        # 4. Appliquer les règles métier
        self._apply_business_rules(order)
        
        # 5. Valider l'ordre
        order.validate()
        
        # 6. Sauvegarder dans la DB (via repository)
        saved_order = self.order_repository.save(order)
        
        # 7. Réduire les stocks
        for item in order.items:
            product = self.product_repository.find_by_id(item.product_id)
            product.reduce_stock(item.quantity)
            self.product_repository.update(product)
        
        return saved_order
    
    def _apply_business_rules(self, order: Order):
        """
        Appliquer les règles métier
        
        Règles :
        - Livraison gratuite si total > 100
        - Discount 10% si total > 500
        """
        total = order.calculate_total()
        
        # Livraison gratuite
        if total > 100:
            order.shipping_cost = 0
        else:
            order.shipping_cost = 10
        
        # Discount
        if total > 500:
            order.discount_percentage = 10
            order.discount_amount = total * 0.1
        else:
            order.discount_percentage = 0
            order.discount_amount = 0
    
    def get_order(self, order_id: int) -> Order:
        """Récupérer une commande"""
        order = self.order_repository.find_by_id(order_id)
        
        if not order:
            raise OrderNotFoundError(f"Order {order_id} not found")
        
        return order
    
    def get_user_orders(
        self, 
        user_id: int, 
        page: int = 1, 
        per_page: int = 10
    ) -> Tuple[List[Order], int]:
        """
        Récupérer les commandes d'un utilisateur avec pagination
        
        Returns:
            Tuple[List[Order], int]: (liste des commandes, total)
        """
        orders = self.order_repository.find_by_user_id(
            user_id=user_id,
            page=page,
            per_page=per_page
        )
        
        total = self.order_repository.count_by_user_id(user_id)
        
        return orders, total
    
    def cancel_order(self, order_id: int) -> Order:
        """
        Annuler une commande
        
        Logique métier :
        - Vérifier que la commande peut être annulée
        - Remettre les stocks
        - Marquer comme annulée
        """
        order = self.get_order(order_id)
        
        # Règle métier : On ne peut annuler que si pending
        if order.status != 'pending':
            raise OrderCannotBeCancelledError(
                f"Order {order_id} cannot be cancelled (status: {order.status})"
            )
        
        # Remettre les stocks
        for item in order.items:
            product = self.product_repository.find_by_id(item.product_id)
            product.increase_stock(item.quantity)
            self.product_repository.update(product)
        
        # Annuler la commande
        order.cancel()
        
        # Sauvegarder
        return self.order_repository.update(order)
```

**business/domain/order.py**

```python
from datetime import datetime
from typing import List

class OrderItem:
    """Entité OrderItem"""
    
    def __init__(
        self,
        product_id: int,
        product_name: str,
        price: float,
        quantity: int
    ):
        self.product_id = product_id
        self.product_name = product_name
        self.price = price
        self.quantity = quantity
    
    def subtotal(self) -> float:
        """Calcule le sous-total"""
        return self.price * self.quantity

class Order:
    """
    Entité Order - Domaine métier
    
    Logique métier pure, pas de dépendances externes
    """
    
    STATUS_PENDING = 'pending'
    STATUS_PAID = 'paid'
    STATUS_SHIPPED = 'shipped'
    STATUS_DELIVERED = 'delivered'
    STATUS_CANCELLED = 'cancelled'
    
    def __init__(
        self,
        user_id: int,
        items: List[OrderItem],
        order_id: int = None,
        status: str = STATUS_PENDING,
        created_at: datetime = None
    ):
        self.id = order_id
        self.user_id = user_id
        self.items = items
        self.status = status
        self.created_at = created_at or datetime.now()
        
        # Calculés par les règles métier
        self.shipping_cost = 10
        self.discount_percentage = 0
        self.discount_amount = 0
    
    def validate(self):
        """Valide la commande"""
        if not self.items:
            raise ValueError("Order must have at least one item")
        
        if not self.user_id:
            raise ValueError("Order must have a user")
        
        for item in self.items:
            if item.quantity <= 0:
                raise ValueError("Quantity must be positive")
            if item.price < 0:
                raise ValueError("Price cannot be negative")
    
    def calculate_total(self) -> float:
        """Calcule le total (sans shipping ni discount)"""
        return sum(item.subtotal() for item in self.items)
    
    def calculate_grand_total(self) -> float:
        """Calcule le total final (avec shipping et discount)"""
        subtotal = self.calculate_total()
        total = subtotal + self.shipping_cost - self.discount_amount
        return max(total, 0)  # Jamais négatif
    
    def cancel(self):
        """Annule la commande"""
        if self.status in [self.STATUS_SHIPPED, self.STATUS_DELIVERED]:
            raise ValueError("Cannot cancel shipped or delivered order")
        
        self.status = self.STATUS_CANCELLED
    
    def mark_as_paid(self):
        """Marque comme payée"""
        if self.status != self.STATUS_PENDING:
            raise ValueError("Can only mark pending orders as paid")
        
        self.status = self.STATUS_PAID
    
    def __repr__(self):
        return f'<Order {self.id} - Status: {self.status}>'
```

---

### COUCHE 3 : ACCÈS AUX DONNÉES

**data_access/repositories/order_repository.py**

```python
from typing import List, Optional
from sqlalchemy.orm import Session
from business.domain.order import Order, OrderItem
from data_access.models.order_model import OrderModel, OrderItemModel
from data_access.mappers.order_mapper import OrderMapper

class OrderRepository:
    """
    Repository - Couche Data Access
    
    Responsabilités :
    - Accéder à la base de données
    - Convertir Model <-> Entity
    - Requêtes SQL optimisées
    
    NE DOIT PAS :
    - Contenir de logique métier
    - Gérer les requêtes HTTP
    """
    
    def __init__(self, session: Session):
        self.session = session
        self.mapper = OrderMapper()
    
    def save(self, order: Order) -> Order:
        """Sauvegarde une commande"""
        
        # Convertir Entity -> Model
        order_model = self.mapper.entity_to_model(order)
        
        # Sauvegarder dans la DB
        self.session.add(order_model)
        self.session.commit()
        self.session.refresh(order_model)
        
        # Convertir Model -> Entity
        return self.mapper.model_to_entity(order_model)
    
    def find_by_id(self, order_id: int) -> Optional[Order]:
        """Trouve une commande par ID"""
        
        # Requête SQL (avec eager loading des items)
        order_model = self.session.query(OrderModel)\
                                  .filter_by(id=order_id)\
                                  .first()
        
        if not order_model:
            return None
        
        # Convertir Model -> Entity
        return self.mapper.model_to_entity(order_model)
    
    def find_by_user_id(
        self, 
        user_id: int, 
        page: int = 1, 
        per_page: int = 10
    ) -> List[Order]:
        """
        Trouve les commandes d'un utilisateur avec pagination
        """
        
        # Calcul offset
        offset = (page - 1) * per_page
        
        # Requête SQL
        order_models = self.session.query(OrderModel)\
                                   .filter_by(user_id=user_id)\
                                   .order_by(OrderModel.created_at.desc())\
                                   .offset(offset)\
                                   .limit(per_page)\
                                   .all()
        
        # Convertir Model -> Entity
        return [self.mapper.model_to_entity(model) for model in order_models]
    
    def count_by_user_id(self, user_id: int) -> int:
        """Compte le nombre de commandes d'un utilisateur"""
        return self.session.query(OrderModel)\
                          .filter_by(user_id=user_id)\
                          .count()
    
    def update(self, order: Order) -> Order:
        """Met à jour une commande"""
        
        # Récupérer le model existant
        order_model = self.session.query(OrderModel)\
                                  .filter_by(id=order.id)\
                                  .first()
        
        if not order_model:
            raise ValueError(f"Order {order.id} not found")
        
        # Mettre à jour les champs
        order_model.status = order.status
        order_model.shipping_cost = order.shipping_cost
        order_model.discount_amount = order.discount_amount
        
        # Sauvegarder
        self.session.commit()
        self.session.refresh(order_model)
        
        return self.mapper.model_to_entity(order_model)
    
    def delete(self, order_id: int) -> bool:
        """Supprime une commande"""
        order_model = self.session.query(OrderModel)\
                                  .filter_by(id=order_id)\
                                  .first()
        
        if not order_model:
            return False
        
        self.session.delete(order_model)
        self.session.commit()
        
        return True
```

**data_access/models/order_model.py**

```python
from sqlalchemy import Column, Integer, Float, String, DateTime, ForeignKey
from sqlalchemy.orm import relationship
from datetime import datetime
from data_access.database import Base

class OrderModel(Base):
    """
    Modèle ORM - Couche Data Access
    
    Représente la table 'orders' en base de données
    """
    
    __tablename__ = 'orders'
    
    id = Column(Integer, primary_key=True)
    user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
    status = Column(String(20), default='pending')
    shipping_cost = Column(Float, default=10.0)
    discount_amount = Column(Float, default=0.0)
    created_at = Column(DateTime, default=datetime.utcnow)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    
    # Relations
    items = relationship('OrderItemModel', back_populates='order', cascade='all, delete-orphan')
    user = relationship('UserModel', back_populates='orders')

class OrderItemModel(Base):
    """Modèle ORM pour OrderItem"""
    
    __tablename__ = 'order_items'
    
    id = Column(Integer, primary_key=True)
    order_id = Column(Integer, ForeignKey('orders.id'), nullable=False)
    product_id = Column(Integer, ForeignKey('products.id'), nullable=False)
    product_name = Column(String(200), nullable=False)
    price = Column(Float, nullable=False)
    quantity = Column(Integer, nullable=False)
    
    # Relations
    order = relationship('OrderModel', back_populates='items')
```

**data_access/mappers/order_mapper.py**

```python
from business.domain.order import Order, OrderItem
from data_access.models.order_model import OrderModel, OrderItemModel

class OrderMapper:
    """
    Mapper - Convertit entre Entity (domain) et Model (ORM)
    
    Responsabilité :
    - Conversion Entity <-> Model
    - Préserver l'isolation entre couches
    """
    
    def model_to_entity(self, model: OrderModel) -> Order:
        """Convertit OrderModel -> Order (Entity)"""
        
        # Convertir les items
        items = [
            OrderItem(
                product_id=item_model.product_id,
                product_name=item_model.product_name,
                price=item_model.price,
                quantity=item_model.quantity
            )
            for item_model in model.items
        ]
        
        # Créer l'entité
        order = Order(
            order_id=model.id,
            user_id=model.user_id,
            items=items,
            status=model.status,
            created_at=model.created_at
        )
        
        # Restaurer les valeurs calculées
        order.shipping_cost = model.shipping_cost
        order.discount_amount = model.discount_amount
        
        return order
    
    def entity_to_model(self, entity: Order) -> OrderModel:
        """Convertit Order (Entity) -> OrderModel"""
        
        # Créer le model
        order_model = OrderModel(
            id=entity.id,
            user_id=entity.user_id,
            status=entity.status,
            shipping_cost=entity.shipping_cost,
            discount_amount=entity.discount_amount,
            created_at=entity.created_at
        )
        
        # Convertir les items
        order_model.items = [
            OrderItemModel(
                product_id=item.product_id,
                product_name=item.product_name,
                price=item.price,
                quantity=item.quantity
            )
            for item in entity.items
        ]
        
        return order_model
```

---

## [SYNC] VARIANTES D'ARCHITECTURE EN COUCHES

### 1. Architecture "Fermée" (Closed Layered) vs "Ouverte" (Open Layered)

**Architecture Fermée (stricte) :**

```
[X] Interdictions :

Présentation -> Métier -> Persistance -> DB
       v         v           v
       X         X           X
       
Présentation NE PEUT PAS sauter Métier pour aller à Persistance
Métier NE PEUT PAS sauter Persistance pour aller à DB

Règle : Chaque couche communique UNIQUEMENT avec la couche adjacente
```

**Avantages :**
- [OK] Isolation maximale
- [OK] Changements localisés
- [OK] Architecture claire

**Inconvénients :**
- [X] Boilerplate (code passeur)
- [X] Performance (chaque couche ajoute de la latence)
- [X] Rigidité

---

**Architecture Ouverte (flexible) :**

```
[OK] Autorisations :

Présentation -> Métier -> Persistance -> DB
       v         v           v
       [OK]         [OK]           [OK]
       
Présentation PEUT sauter Métier dans certains cas justifiés
(ex: export CSV, streaming)

Règle : Généralement fermée, mais ouverte pour les cas d'usage spécifiques
```

**Exemple d'utilisation :**

```python
# Cas normal : Passer par toutes les couches
@app.route('/orders/<order_id>')
def get_order(order_id):
    order = order_service.get_order(order_id)  # Via service
    return jsonify(order.to_dict())

# Cas spécial : Accès direct pour optimisation
@app.route('/export-orders-csv')
def export_orders():
    # Accès direct au repository pour streaming
    # (sauter le service qui chargerait tout en mémoire)
    repository = OrderRepository()
    
    def generate():
        for order in repository.stream_all():  # Streaming
            yield format_csv_row(order)
    
    return Response(generate(), mimetype='text/csv')
    # [OK] Justifié : Performance critique
```

---

### 2. Architecture avec couche de Services et de Domain séparés

```
Présentation
    v
Application Services (Use Cases)
    v
Domain Services (Règles métier)
    v
Domain Model (Entités)
    v
Repositories
    v
Database

Exemple :
- Application Service : CreateOrderUseCase (orchestration)
- Domain Service : PricingService (calcul de prix)
- Domain Model : Order, Product (entités)
```

---

## [TEST] TESTS EN ARCHITECTURE EN COUCHES

### Tests par couche

**Test Couche Présentation (Controller) :**

```python
# test_order_controller.py
def test_create_order_success():
    # Arrange : Mock du service
    mock_service = Mock()
    mock_order = Order(user_id=1, items=[...])
    mock_service.create_order.return_value = mock_order
    
    controller = OrderController(mock_service)
    
    # Act
    app.test_request_context(
        '/orders',
        method='POST',
        json={'user_id': 1, 'items': [...]}
    )
    response = controller.create_order()
    
    # Assert
    assert response[1] == 201  # Status code
    mock_service.create_order.assert_called_once()

def test_create_order_invalid_data():
    mock_service = Mock()
    controller = OrderController(mock_service)
    
    # Données invalides
    app.test_request_context(
        '/orders',
        method='POST',
        json={'user_id': 1}  # Manque 'items'
    )
    response = controller.create_order()
    
    # Assert
    assert response[1] == 400  # Bad Request
    mock_service.create_order.assert_not_called()
```

---

**Test Couche Métier (Service) :**

```python
# test_order_service.py
def test_create_order_success():
    # Arrange : Mocks des repositories
    mock_order_repo = Mock()
    mock_product_repo = Mock()
    mock_user_repo = Mock()
    
    # User existe
    mock_user = User(id=1, email='test@example.com')
    mock_user_repo.find_by_id.return_value = mock_user
    
    # Product existe avec stock
    mock_product = Product(id=1, name='Product 1', price=10.0, stock=100)
    mock_product_repo.find_by_id.return_value = mock_product
    
    # Service avec mocks
    service = OrderService(mock_order_repo, mock_product_repo, mock_user_repo)
    
    # Act
    order = service.create_order(
        user_id=1,
        items=[{'product_id': 1, 'quantity': 2}]
    )
    
    # Assert
    assert order.user_id == 1
    assert len(order.items) == 1
    assert order.items[0].quantity == 2
    mock_order_repo.save.assert_called_once()
    mock_product_repo.update.assert_called_once()  # Stock réduit

def test_create_order_insufficient_stock():
    # Arrange
    mock_order_repo = Mock()
    mock_product_repo = Mock()
    mock_user_repo = Mock()
    
    mock_user_repo.find_by_id.return_value = User(id=1)
    
    # Product avec stock insuffisant
    mock_product = Product(id=1, name='Product 1', price=10.0, stock=1)
    mock_product_repo.find_by_id.return_value = mock_product
    
    service = OrderService(mock_order_repo, mock_product_repo, mock_user_repo)
    
    # Act & Assert
    with pytest.raises(InsufficientStockError):
        service.create_order(
            user_id=1,
            items=[{'product_id': 1, 'quantity': 10}]  # Demande 10, stock = 1
        )
    
    # Vérifier que la commande n'a PAS été sauvegardée
    mock_order_repo.save.assert_not_called()
```

---

**Test Couche Persistance (Repository) :**

```python
# test_order_repository.py
def test_save_order():
    # Arrange : DB de test
    test_session = create_test_database_session()
    repository = OrderRepository(test_session)
    
    # Créer une commande
    order = Order(
        user_id=1,
        items=[
            OrderItem(product_id=1, product_name='Product 1', price=10.0, quantity=2)
        ]
    )
    
    # Act
    saved_order = repository.save(order)
    
    # Assert
    assert saved_order.id is not None  # ID généré
    
    # Vérifier en DB
    order_from_db = test_session.query(OrderModel).filter_by(id=saved_order.id).first()
    assert order_from_db is not None
    assert order_from_db.user_id == 1
    assert len(order_from_db.items) == 1

def test_find_by_user_id():
    test_session = create_test_database_session()
    repository = OrderRepository(test_session)
    
    # Créer des commandes de test
    order1 = Order(user_id=1, items=[...])
    order2 = Order(user_id=1, items=[...])
    order3 = Order(user_id=2, items=[...])  # Autre user
    
    repository.save(order1)
    repository.save(order2)
    repository.save(order3)
    
    # Act
    user1_orders = repository.find_by_user_id(user_id=1)
    
    # Assert
    assert len(user1_orders) == 2
    assert all(order.user_id == 1 for order in user1_orders)
```

---

## [OBJECTIF] QUAND UTILISER L'ARCHITECTURE EN COUCHES ?

### [OK] Utilise l'architecture en couches SI :

```
1. Application de TAILLE MOYENNE à GRANDE
   - > 10,000 lignes de code
   - Plusieurs fonctionnalités
   - Équipe > 3 développeurs

2. Besoin de MAINTENABILITÉ sur le LONG TERME
   - Projet > 1 an
   - Évolutions fréquentes
   - Plusieurs versions à maintenir

3. Logique MÉTIER IMPORTANTE
   - Règles business complexes
   - Calculs métier
   - Workflows

4. Besoin de TESTS ROBUSTES
   - Couverture de tests élevée requise
   - Tests automatisés
   - CI/CD

5. Équipe qui GRANDIT
   - Division du travail
   - Spécialisation
   - Onboarding facilité

6. Changements de TECHNOLOGIE possibles
   - Migration de DB prévue
   - Changement de framework possible
   - Besoin de flexibilité
```

---

### [X] N'utilise PAS l'architecture en couches SI :

```
1. Projet TRÈS PETIT
   - < 1,000 lignes de code
   - MVP rapide
   - Prototype

2. Application SIMPLE (CRUD basique)
   - Pas de logique métier
   - Juste affichage de données
   - Pas d'évolution prévue

3. Time-to-market CRITIQUE
   - Besoin de sortir vite
   - Validation d'idée
   - Démo

4. Équipe TRÈS PETITE
   - 1-2 développeurs
   - Pas d'expertise architecture
   - Budget limité

5. Application ÉPHÉMÈRE
   - Script one-shot
   - Outil interne temporaire
   - Pas de maintenance

COMMENCE SIMPLE,
puis ajoute des couches SI NÉCESSAIRE
```

---

## [BRAVO] CONCLUSION

### Ce que tu as appris

**Architecture en Couches :**
- [OK] Organisation horizontale en couches
- [OK] 4 couches principales (Présentation, Métier, Persistance, DB)
- [OK] Séparation des préoccupations
- [OK] Communication entre couches adjacentes
- [OK] Variantes (fermée vs ouverte)

**Pratique :**
- [OK] Exemple e-commerce complet
- [OK] Code de toutes les couches
- [OK] DTOs, Mappers, Services, Repositories
- [OK] Tests par couche

---

### Points clés

```
[OBJECTIF] Architecture en Couches = Organisation horizontale
[OBJECTIF] Chaque couche = Responsabilité spécifique
[OBJECTIF] Communication descendante (ou adjacente)
[OBJECTIF] Facile à comprendre et maintenir
[OBJECTIF] Standard de l'industrie
[OBJECTIF] Base de nombreuses autres architectures
[OBJECTIF] Équilibre entre simplicité et structure
```

---

### Comparaison rapide

| Architecture | Séparation | Complexité | Maintenance |
|--------------|------------|------------|-------------|
| **Tout dans un fichier** | [X] Aucune | * Très simple | [X] Difficile |
| **MVC** | [OK] M-V-C | ** Simple | [OK] Facile |
| **Layered (4 couches)** | [OK][OK] Horizontale | *** Moyenne | [OK][OK] Très facile |
| **Clean Architecture** | [OK][OK][OK] Cercles | **** Élevée | [OK][OK][OK] Excellente |

---

### Prochaines étapes

1. Code l'exemple e-commerce
2. Compare avec MVC (similitudes/différences)
3. Compare avec Clean Architecture
4. Lis `architecture_hexagonale.txt`

**[RAPIDE] Tu maîtrises maintenant l'Architecture en Couches ! [RAPIDE]**

═══════════════════════════════════════════════════════════════
FIN DU FICHIER : architecture_layered.txt
═══════════════════════════════════════════════════════════════

# [WHITE_HEXAGON] ARCHITECTURE HEXAGONALE (PORTS & ADAPTERS) - GUIDE ULTRA DÉTAILLÉ

## * INTRODUCTION

### Qu'est-ce que l'architecture hexagonale ?

**Architecture Hexagonale** = Pattern créé par **Alistair Cockburn** qui place la **logique métier au centre** et isole complètement les **détails techniques** via des **ports** (interfaces) et des **adaptateurs** (implémentations).

Aussi appelée : **Ports & Adapters Architecture**

**Analogie : Prise électrique universelle [PLUGIN]**

```
[RAPIDE] APPAREIL (Logique métier)
   └─ [PLUGIN] Port (Interface standardisée)
       └─ [OUTIL] Adaptateur (Implémentation spécifique)
           └─ [ACCUEIL] Source d'énergie (Détail technique)

Exemples :
[MOBILE] Téléphone (appareil)
   └─ Port USB-C (standard)
       ├─ Adaptateur mural -> Prise 220V
       ├─ Adaptateur voiture -> 12V
       ├─ Adaptateur solaire -> Panneau
       └─ Adaptateur batterie -> Powerbank

L'appareil ne CONNAÎT que le port USB-C
Il ne sait PAS d'où vient l'électricité
On peut changer la source sans modifier l'appareil
```

**En informatique :**

```
          [OUTIL] Adaptateur Web      [OUTIL] Adaptateur CLI
               (Flask)              (Terminal)
                  v                      v
              [PLUGIN] Port HTTP          [PLUGIN] Port CLI
              (Interface)           (Interface)
                  v                      v
        ┌─────────────────────────────────────┐
        │     [WHITE_HEXAGON] APPLICATION CORE [WHITE_HEXAGON]           │
        │   (Logique métier pure)             │
        │   - Use Cases                       │
        │   - Domain Logic                    │
        │   - Business Rules                  │
        └─────────────────────────────────────┘
                  v                      v
           [PLUGIN] Port Database      [PLUGIN] Port Email
           (Interface)            (Interface)
                  v                      v
        [OUTIL] Adaptateur SQL     [OUTIL] Adaptateur SMTP
           (PostgreSQL)          (Gmail)

RÈGLE D'OR : Le Core ne connaît QUE les ports (interfaces)
            Les adaptateurs se branchent sur les ports
            On peut changer les adaptateurs sans toucher le Core
```

---

### [MESURE] Structure en Hexagone

**Pourquoi un hexagone ? [REFLEXION]**

```
Le nom "hexagone" est SYMBOLIQUE :
- Représente la symétrie
- Pas de "haut" ou "bas" (contrairement aux couches)
- Plusieurs faces = plusieurs ports
- Le nombre 6 n'est pas important (peut être 5, 7, 8...)

        Adaptateur Web
              v
         ┌────────┐
    API ─│        │─ CLI
         │  CORE  │
    DB ──│        │── Email
         └────────┘
              v
       Adaptateur Tests

Chaque côté de l'hexagone = un port
Chaque adaptateur se branche sur un port
```

---

### [OBJECTIF] Concepts clés

**1. Application Core (Cœur applicatif) [GEM_STONE]**

```
Le CENTRE de l'hexagone

Contient :
[OK] Logique métier pure
[OK] Use Cases
[OK] Entités du domaine
[OK] Règles business
[OK] Ports (interfaces)

Ne contient PAS :
[X] Framework (Flask, Django, etc.)
[X] Base de données (SQL, MongoDB, etc.)
[X] UI (HTML, React, etc.)
[X] Services externes (Email, Payment, etc.)

Principe : Le Core ne dépend de RIEN d'externe
```

---

**2. Ports (Interfaces) [PLUGIN]**

```
DEUX types de ports :

[ENTREE] PORTS PRIMAIRES (Driving Ports / Inbound)
   -> Le monde extérieur APPELLE le Core
   -> Exemple : API HTTP, CLI, Tests
   
   Interface définie PAR le Core
   Implémentation DANS le Core

[SORTIE] PORTS SECONDAIRES (Driven Ports / Outbound)
   -> Le Core APPELLE le monde extérieur
   -> Exemple : Database, Email, Payment
   
   Interface définie PAR le Core
   Implémentation HORS du Core (adaptateurs)
```

**Exemple :**

```python
# PORT PRIMAIRE (défini et implémenté dans le Core)
class OrderUseCase:
    """Port primaire : Use Case pour créer une commande"""
    
    def create_order(self, user_id: int, items: List) -> Order:
        # Logique métier
        pass

# PORT SECONDAIRE (interface définie dans le Core)
class OrderRepository(ABC):
    """Port secondaire : Interface pour persister les commandes"""
    
    @abstractmethod
    def save(self, order: Order) -> Order:
        pass
    
    @abstractmethod
    def find_by_id(self, order_id: int) -> Optional[Order]:
        pass
```

---

**3. Adaptateurs (Adapters) [OUTIL]**

```
DEUX types d'adaptateurs :

[OUTIL] ADAPTATEURS PRIMAIRES (Driving Adapters)
   -> Appellent le Core via les ports primaires
   -> Vivent HORS du Core
   -> Exemples :
      - Adaptateur Web (Flask, FastAPI)
      - Adaptateur CLI
      - Adaptateur Tests
      - Adaptateur GraphQL

[OUTIL] ADAPTATEURS SECONDAIRES (Driven Adapters)
   -> Implémentent les ports secondaires
   -> Vivent HORS du Core
   -> Exemples :
      - Adaptateur PostgreSQL
      - Adaptateur MongoDB
      - Adaptateur SMTP
      - Adaptateur Stripe (Payment)
```

**Exemple :**

```python
# ADAPTATEUR PRIMAIRE (Web)
@app.route('/orders', methods=['POST'])
def create_order_endpoint():
    """Adaptateur Web qui appelle le Use Case"""
    data = request.json
    
    # Appeler le port primaire (Use Case)
    order = order_use_case.create_order(
        user_id=data['user_id'],
        items=data['items']
    )
    
    return jsonify(order.to_dict()), 201

# ADAPTATEUR SECONDAIRE (Database)
class PostgreSQLOrderRepository(OrderRepository):
    """Adaptateur PostgreSQL qui implémente le port"""
    
    def save(self, order: Order) -> Order:
        # Implémentation spécifique PostgreSQL
        conn = psycopg2.connect(...)
        cursor.execute("INSERT INTO orders ...")
        return order
```

---

## [REFLEXION] POURQUOI UTILISER L'ARCHITECTURE HEXAGONALE ?

### [OK] Avantages

**1. Indépendance TOTALE du Core [GEM_STONE]**

**Problème avec architecture traditionnelle :**

```python
# [X] Logique métier couplée au framework Django
from django.db import models

class Order(models.Model):
    """Entité complètement couplée à Django ORM"""
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    total = models.DecimalField(max_digits=10, decimal_places=2)
    
    def calculate_discount(self):
        # Logique métier mélangée avec Django
        if self.total > 100:
            return self.total * 0.1
        return 0

# Problèmes :
# [X] Impossible de tester sans Django
# [X] Impossible d'utiliser hors de Django
# [X] Changer de framework = tout réécrire
```

---

**Avec architecture hexagonale :**

```python
# [OK] CORE (100% indépendant)
# domain/entities/order.py
class Order:
    """Entité pure - ZÉRO dépendance externe"""
    
    def __init__(self, user_id: int, items: List[OrderItem]):
        self.user_id = user_id
        self.items = items
    
    def calculate_total(self) -> float:
        """Logique métier pure"""
        return sum(item.price * item.quantity for item in self.items)
    
    def calculate_discount(self) -> float:
        """Logique métier pure"""
        total = self.calculate_total()
        if total > 100:
            return total * 0.1
        return 0
    
    # Pas de dépendance à Django, Flask, SQLAlchemy, etc.
    # Testable directement
    # Réutilisable dans n'importe quel contexte

# [PLUGIN] PORT (Interface définie par le Core)
# ports/order_repository.py
class OrderRepository(ABC):
    @abstractmethod
    def save(self, order: Order) -> Order:
        pass

# [OUTIL] ADAPTATEUR Django (si on utilise Django)
# adapters/django_order_repository.py
class DjangoOrderRepository(OrderRepository):
    def save(self, order: Order) -> Order:
        DjangoOrderModel.objects.create(
            user_id=order.user_id,
            total=order.calculate_total()
        )
        return order

# [OUTIL] ADAPTATEUR Flask/SQLAlchemy (alternative)
# adapters/sqlalchemy_order_repository.py
class SQLAlchemyOrderRepository(OrderRepository):
    def save(self, order: Order) -> Order:
        model = OrderModel(
            user_id=order.user_id,
            total=order.calculate_total()
        )
        db.session.add(model)
        db.session.commit()
        return order

# [OK] Le Core reste identique
# [OK] On change juste l'adaptateur
# [OK] Migration Django -> Flask = changer 1 ligne :
# order_repository = DjangoOrderRepository()  # Avant
# order_repository = SQLAlchemyOrderRepository()  # Après
```

---

**2. Testabilité maximale [TEST]**

**Sans architecture hexagonale :**

```python
# [X] Test difficile (dépendances partout)
def test_create_order():
    # Il faut :
    # 1. Démarrer Flask
    # 2. Connecter à une vraie DB
    # 3. Créer des données de test
    # 4. Faire une requête HTTP
    # 5. Nettoyer la DB
    
    app = create_app()
    client = app.test_client()
    
    # Setup DB
    db.create_all()
    test_user = User.create(...)
    test_product = Product.create(...)
    
    # Test
    response = client.post('/orders', json={...})
    
    # Cleanup
    db.drop_all()
    
    # [!] Lent, complexe, fragile
```

---

**Avec architecture hexagonale :**

```python
# [OK] TEST DU CORE (sans aucune dépendance)
def test_order_calculate_discount():
    # Pas besoin de DB, Framework, HTTP, etc.
    
    # Arrange
    order = Order(
        user_id=1,
        items=[
            OrderItem(price=60, quantity=1),
            OrderItem(price=50, quantity=1)
        ]
    )
    
    # Act
    discount = order.calculate_discount()
    
    # Assert
    assert discount == 11.0  # 10% de 110
    
    # [OK] Ultra rapide (microseconde)
    # [OK] Zéro dépendance
    # [OK] 100% fiable

# [OK] TEST DU USE CASE (avec mocks)
def test_create_order_use_case():
    # Mock des ports secondaires
    mock_order_repo = Mock(spec=OrderRepository)
    mock_product_repo = Mock(spec=ProductRepository)
    
    # Use Case avec mocks
    use_case = CreateOrderUseCase(mock_order_repo, mock_product_repo)
    
    # Test
    order = use_case.execute(user_id=1, items=[...])
    
    # Vérifications
    assert order.calculate_total() == 110
    mock_order_repo.save.assert_called_once()
    
    # [OK] Rapide (millisecondes)
    # [OK] Pas de DB réelle

# [OK] TEST D'ADAPTATEUR (isolation)
def test_postgresql_adapter():
    # Tester UNIQUEMENT l'adaptateur
    # Avec une DB de test
    
    test_db = create_test_database()
    adapter = PostgreSQLOrderRepository(test_db)
    
    order = Order(user_id=1, items=[...])
    saved_order = adapter.save(order)
    
    assert saved_order.id is not None
    
    # [OK] Test d'intégration isolé
```

---

**3. Substitution facile des adaptateurs [SYNC]**

**Scénario : Migration de base de données**

```python
# Configuration actuelle (PostgreSQL)
order_repository = PostgreSQLOrderRepository(postgres_connection)

use_case = CreateOrderUseCase(
    order_repository=order_repository,
    product_repository=product_repository
)

# ─────────────────────────────────────────────────

# Migration vers MongoDB (juste changer l'adaptateur)
order_repository = MongoDBOrderRepository(mongo_connection)

use_case = CreateOrderUseCase(
    order_repository=order_repository,  # <- Même interface !
    product_repository=product_repository
)

# [OK] Le Core ne change PAS
# [OK] Les Use Cases ne changent PAS
# [OK] Seul l'adaptateur change
# [OK] Tests du Core toujours valides
```

---

**Scénario : Plusieurs implémentations en parallèle**

```python
# Configuration production : PostgreSQL
production_repo = PostgreSQLOrderRepository(prod_db)

# Configuration développement : In-Memory (rapide)
dev_repo = InMemoryOrderRepository()

# Configuration tests : SQLite
test_repo = SQLiteOrderRepository(':memory:')

# Configuration cache : Avec cache Redis
cached_repo = CachedOrderRepository(
    base_repository=production_repo,
    cache=redis_client
)

# [OK] Même interface OrderRepository
# [OK] On choisit selon le contexte
# [OK] Aucun changement dans le Core
```

---

**4. Symétrie (pas de "haut" ou "bas") [SCALES]**

**Architecture en couches (hiérarchie) :**

```
COUCHE 1 : Présentation (en "haut")
    v (dépend de)
COUCHE 2 : Métier
    v (dépend de)
COUCHE 3 : Persistance
    v (dépend de)
COUCHE 4 : Base de données (en "bas")

[X] Asymétrie : Il y a un "haut" et un "bas"
[X] La présentation est "supérieure" à la DB (conceptuellement)
```

---

**Architecture hexagonale (symétrie) :**

```
         Adaptateur Web
              v
         [PLUGIN] Port HTTP
              v
        ┌──────────┐
   API ─│   CORE   │─ CLI
        │ (Centre) │
    DB ─│          │─ Email
        └──────────┘
              v
        [PLUGIN] Port Tests
              v
       Adaptateur Tests

[OK] Symétrie totale
[OK] Pas de hiérarchie
[OK] Tous les adaptateurs sont équivalents
[OK] Le Core est au centre, tous les adaptateurs autour
```

**Conséquence :**

```
Le Core voit tous les adaptateurs de la même manière :
- L'adaptateur Web n'est PAS plus important que l'adaptateur DB
- L'adaptateur CLI a la même importance que l'adaptateur Email
- Tous communiquent via des ports (interfaces)
```

---

**5. Facilite le TDD (Test-Driven Development) [TEST]**

**Workflow TDD avec Hexagonal :**

```
ÉTAPE 1 : Écrire le test (Use Case)
──────────────────────────────────
def test_create_order():
    mock_repo = Mock()
    use_case = CreateOrderUseCase(mock_repo)
    
    order = use_case.execute(user_id=1, items=[...])
    
    assert order is not None
    mock_repo.save.assert_called_once()

ÉTAPE 2 : Écrire le Use Case (dans le Core)
──────────────────────────────────
class CreateOrderUseCase:
    def execute(self, user_id, items):
        order = Order(user_id, items)
        order.validate()
        return self.order_repository.save(order)

ÉTAPE 3 : Tester (passe [OK])
──────────────────────────────────

ÉTAPE 4 : Écrire l'adaptateur (plus tard)
──────────────────────────────────
class PostgreSQLOrderRepository:
    def save(self, order):
        # Implémentation SQL
        pass

[OK] On peut développer le Core SANS les adaptateurs
[OK] Tests ultra rapides (pas de DB)
[OK] Focus sur la logique métier
[OK] Les adaptateurs viennent après
```

---

### [X] Inconvénients

**1. Complexité initiale [DOCS]**

**Pour un projet simple :**

```python
# Sans Hexagonal (simple, 1 fichier)
# app.py - 50 lignes
@app.route('/orders', methods=['POST'])
def create_order():
    data = request.json
    
    order = Order(data['user_id'], data['items'])
    db.session.add(order)
    db.session.commit()
    
    return jsonify({'id': order.id}), 201

# [OK] Simple, direct, fonctionne
```

---

```
Avec Hexagonal (complexe, 10+ fichiers)

core/
  domain/
    entities/
      order.py
  use_cases/
    create_order.py
  ports/
    order_repository.py

adapters/
  primary/
    web/
      order_controller.py
  secondary/
    postgresql/
      order_repository_impl.py

# [!] 10+ fichiers pour créer une commande
# [X] Overkill pour un projet simple
```

---

**2. Courbe d'apprentissage [HAUSSE]**

**Concepts à maîtriser :**

```
Débutant :
├─ Séparation Core vs Adapters
├─ Concept de Ports (interfaces)
├─ Dependency Inversion
├─ Injection de dépendances
└─ Ports primaires vs secondaires

Intermédiaire :
├─ Use Cases
├─ Domain-Driven Design
├─ Adaptateurs multiples
└─ Tests avec mocks

Avancé :
├─ Event-Driven Hexagonal
├─ CQRS dans Hexagonal
└─ Domain Events

Temps d'apprentissage : 4-6 semaines
```

---

**3. Plus de code (boilerplate) [NOTE]**

**Exemple : Récupérer un utilisateur**

```python
# Sans Hexagonal (direct)
@app.route('/users/<user_id>')
def get_user(user_id):
    user = User.query.get(user_id)
    return jsonify(user.to_dict())

# 3 lignes [OK]

# ─────────────────────────────────────────────────

# Avec Hexagonal

# 1. Port (interface)
class UserRepository(ABC):
    @abstractmethod
    def find_by_id(self, user_id: int) -> Optional[User]:
        pass

# 2. Use Case
class GetUserUseCase:
    def __init__(self, user_repository: UserRepository):
        self.user_repository = user_repository
    
    def execute(self, user_id: int) -> User:
        return self.user_repository.find_by_id(user_id)

# 3. Adaptateur Repository
class SQLAlchemyUserRepository(UserRepository):
    def find_by_id(self, user_id: int) -> Optional[User]:
        return User.query.get(user_id)

# 4. Adaptateur Web
@app.route('/users/<user_id>')
def get_user(user_id):
    user = get_user_use_case.execute(user_id)
    return jsonify(user.to_dict())

# 30+ lignes [NEUTRAL_FACE]

# [OK] Avantages : Testable, Flexible, Maintenable
# [X] Inconvénient : Beaucoup plus de code
```

---

**4. Peut sembler sur-ingéniéré [CONSTRUCTION]**

```
Pour un CRUD simple, Hexagonal semble excessif :

GET /users -> Use Case -> Repository -> DB
POST /users -> Use Case -> Repository -> DB
PUT /users -> Use Case -> Repository -> DB
DELETE /users -> Use Case -> Repository -> DB

Toutes les routes font la même chose :
"Appeler Use Case qui appelle Repository"

Pour un CRUD sans logique métier,
l'architecture hexagonale ajoute de la complexité sans bénéfice.
```

**Quand c'est justifié :**
- Logique métier complexe
- Besoin de flexibilité future
- Tests critiques
- Plusieurs interfaces (Web + CLI + API)

**Quand c'est overkill :**
- CRUD basique
- Pas de logique métier
- Pas de tests
- Projet jetable

---

## [CONSTRUCTION] STRUCTURE COMPLÈTE D'UN PROJET HEXAGONAL

### [DOSSIER] Organisation recommandée

```
hexagonal_app/
│
├── core/                           # [WHITE_HEXAGON] APPLICATION CORE
│   ├── __init__.py
│   │
│   ├── domain/                     # Entités & Logique métier
│   │   ├── __init__.py
│   │   ├── entities/
│   │   │   ├── __init__.py
│   │   │   ├── user.py
│   │   │   ├── product.py
│   │   │   └── order.py
│   │   │
│   │   ├── value_objects/
│   │   │   ├── __init__.py
│   │   │   ├── email.py
│   │   │   └── money.py
│   │   │
│   │   └── exceptions/
│   │       ├── __init__.py
│   │       └── domain_exceptions.py
│   │
│   ├── use_cases/                  # Use Cases (logique applicative)
│   │   ├── __init__.py
│   │   ├── user/
│   │   │   ├── create_user.py
│   │   │   ├── get_user.py
│   │   │   └── update_user.py
│   │   └── order/
│   │       ├── create_order.py
│   │       ├── cancel_order.py
│   │       └── get_order_history.py
│   │
│   └── ports/                      # [PLUGIN] PORTS (Interfaces)
│       ├── __init__.py
│       │
│       ├── primary/                # [ENTREE] Ports primaires (Inbound)
│       │   ├── __init__.py
│       │   └── user_management.py  # Interface Use Case
│       │
│       └── secondary/              # [SORTIE] Ports secondaires (Outbound)
│           ├── __init__.py
│           ├── repositories/
│           │   ├── user_repository.py
│           │   ├── product_repository.py
│           │   └── order_repository.py
│           │
│           └── services/
│               ├── email_service.py
│               └── payment_service.py
│
├── adapters/                       # [OUTIL] ADAPTATEURS
│   ├── __init__.py
│   │
│   ├── primary/                    # [OUTIL] Adaptateurs primaires (Driving)
│   │   ├── __init__.py
│   │   │
│   │   ├── web/                    # Adaptateur Web (Flask/FastAPI)
│   │   │   ├── __init__.py
│   │   │   ├── app.py
│   │   │   ├── controllers/
│   │   │   │   ├── user_controller.py
│   │   │   │   └── order_controller.py
│   │   │   └── dto/
│   │   │       └── request_dto.py
│   │   │
│   │   ├── cli/                    # Adaptateur CLI
│   │   │   ├── __init__.py
│   │   │   └── commands.py
│   │   │
│   │   └── graphql/                # Adaptateur GraphQL (optionnel)
│   │       ├── __init__.py
│   │       └── schema.py
│   │
│   └── secondary/                  # [OUTIL] Adaptateurs secondaires (Driven)
│       ├── __init__.py
│       │
│       ├── repositories/           # Implémentations repositories
│       │   ├── __init__.py
│       │   ├── postgresql/
│       │   │   ├── user_repository_impl.py
│       │   │   └── order_repository_impl.py
│       │   │
│       │   ├── mongodb/            # Alternative MongoDB
│       │   │   └── user_repository_impl.py
│       │   │
│       │   └── in_memory/          # Pour les tests
│       │       └── user_repository_impl.py
│       │
│       └── services/               # Implémentations services
│           ├── smtp_email_service.py
│           └── stripe_payment_service.py
│
├── config/                         # Configuration
│   ├── __init__.py
│   ├── dependency_injection.py    # Container DI
│   └── settings.py
│
├── tests/                          # Tests
│   ├── unit/                       # Tests unitaires (Core)
│   │   ├── domain/
│   │   └── use_cases/
│   │
│   ├── integration/                # Tests d'intégration
│   │   └── adapters/
│   │
│   └── e2e/                        # Tests end-to-end
│
├── app.py                          # Point d'entrée
├── requirements.txt
└── README.md
```

---

## [CODE] CODE COMPLET - EXEMPLE E-COMMERCE

### [WHITE_HEXAGON] CORE (Application Core)

**core/domain/entities/order.py**

```python
from datetime import datetime
from typing import List
from core.domain.value_objects.money import Money
from core.domain.exceptions.domain_exceptions import InvalidOrderError

class OrderItem:
    """Item de commande - Value Object"""
    
    def __init__(
        self,
        product_id: int,
        product_name: str,
        price: Money,
        quantity: int
    ):
        if quantity <= 0:
            raise ValueError("Quantity must be positive")
        
        self.product_id = product_id
        self.product_name = product_name
        self.price = price
        self.quantity = quantity
    
    def subtotal(self) -> Money:
        """Calcule le sous-total"""
        return Money(
            amount=self.price.amount * self.quantity,
            currency=self.price.currency
        )

class Order:
    """
    Entité Order - Domain Core
    
    Logique métier PURE
    Aucune dépendance externe
    """
    
    STATUS_PENDING = 'pending'
    STATUS_CONFIRMED = 'confirmed'
    STATUS_SHIPPED = 'shipped'
    STATUS_DELIVERED = 'delivered'
    STATUS_CANCELLED = 'cancelled'
    
    def __init__(
        self,
        user_id: int,
        items: List[OrderItem],
        order_id: int = None,
        status: str = STATUS_PENDING,
        created_at: datetime = None
    ):
        self.id = order_id
        self.user_id = user_id
        self.items = items
        self.status = status
        self.created_at = created_at or datetime.now()
        
        # Règles métier appliquées
        self.shipping_cost = self._calculate_shipping()
        self.discount = self._calculate_discount()
    
    def validate(self):
        """Valide la commande selon les règles métier"""
        if not self.items:
            raise InvalidOrderError("Order must have at least one item")
        
        if not self.user_id:
            raise InvalidOrderError("Order must have a user")
        
        for item in self.items:
            if item.quantity <= 0:
                raise InvalidOrderError("Item quantity must be positive")
    
    def calculate_subtotal(self) -> Money:
        """Calcule le sous-total (sans frais)"""
        total = sum(item.subtotal().amount for item in self.items)
        currency = self.items[0].price.currency
        return Money(total, currency)
    
    def _calculate_shipping(self) -> Money:
        """
        Règle métier : Calcul des frais de livraison
        - Gratuit si subtotal > 100
        - Sinon 10€
        """
        subtotal = self.calculate_subtotal()
        
        if subtotal.amount >= 100:
            return Money(0, subtotal.currency)
        
        return Money(10, subtotal.currency)
    
    def _calculate_discount(self) -> Money:
        """
        Règle métier : Calcul de la réduction
        - 10% si subtotal > 500
        - 5% si subtotal > 200
        - 0 sinon
        """
        subtotal = self.calculate_subtotal()
        
        if subtotal.amount > 500:
            return Money(subtotal.amount * 0.10, subtotal.currency)
        elif subtotal.amount > 200:
            return Money(subtotal.amount * 0.05, subtotal.currency)
        
        return Money(0, subtotal.currency)
    
    def calculate_total(self) -> Money:
        """Calcule le total final"""
        subtotal = self.calculate_subtotal()
        
        total = (
            subtotal.amount +
            self.shipping_cost.amount -
            self.discount.amount
        )
        
        return Money(max(total, 0), subtotal.currency)
    
    def confirm(self):
        """Confirme la commande"""
        if self.status != self.STATUS_PENDING:
            raise InvalidOrderError(
                f"Cannot confirm order with status {self.status}"
            )
        
        self.status = self.STATUS_CONFIRMED
    
    def cancel(self):
        """Annule la commande"""
        if self.status in [self.STATUS_SHIPPED, self.STATUS_DELIVERED]:
            raise InvalidOrderError(
                f"Cannot cancel order with status {self.status}"
            )
        
        self.status = self.STATUS_CANCELLED
    
    def ship(self):
        """Expédie la commande"""
        if self.status != self.STATUS_CONFIRMED:
            raise InvalidOrderError(
                f"Cannot ship order with status {self.status}"
            )
        
        self.status = self.STATUS_SHIPPED
    
    def __repr__(self):
        return f'<Order {self.id} - {self.status}>'
```

---

**core/use_cases/order/create_order.py**

```python
from typing import List, Dict
from core.domain.entities.order import Order, OrderItem
from core.domain.value_objects.money import Money
from core.ports.secondary.repositories.order_repository import OrderRepository
from core.ports.secondary.repositories.product_repository import ProductRepository
from core.ports.secondary.services.email_service import EmailService
from core.domain.exceptions.domain_exceptions import *

class CreateOrderUseCase:
    """
    Use Case : Créer une commande
    
    Port primaire (interface implémentée par le Core)
    Les adaptateurs primaires appellent ce Use Case
    """
    
    def __init__(
        self,
        order_repository: OrderRepository,
        product_repository: ProductRepository,
        email_service: EmailService
    ):
        # Dépend des PORTS (interfaces), pas des implémentations
        self.order_repository = order_repository
        self.product_repository = product_repository
        self.email_service = email_service
    
    def execute(
        self,
        user_id: int,
        items: List[Dict]
    ) -> Order:
        """
        Exécute le Use Case
        
        Args:
            user_id: ID de l'utilisateur
            items: Liste d'items [{'product_id': 1, 'quantity': 2}, ...]
            
        Returns:
            Order: Commande créée
            
        Raises:
            ProductNotFoundError: Produit non trouvé
            InsufficientStockError: Stock insuffisant
            InvalidOrderError: Commande invalide
        """
        
        # 1. Valider et récupérer les produits
        order_items = []
        
        for item_data in items:
            product_id = item_data['product_id']
            quantity = item_data['quantity']
            
            # Récupérer le produit via le port (repository)
            product = self.product_repository.find_by_id(product_id)
            
            if not product:
                raise ProductNotFoundError(
                    f"Product {product_id} not found"
                )
            
            # Vérifier le stock
            if not product.has_sufficient_stock(quantity):
                raise InsufficientStockError(
                    f"Insufficient stock for {product.name}. "
                    f"Available: {product.stock}, Requested: {quantity}"
                )
            
            # Créer l'OrderItem
            order_item = OrderItem(
                product_id=product.id,
                product_name=product.name,
                price=Money(product.price, 'EUR'),
                quantity=quantity
            )
            order_items.append(order_item)
        
        # 2. Créer l'entité Order
        order = Order(
            user_id=user_id,
            items=order_items
        )
        
        # 3. Valider la commande (règles métier)
        order.validate()
        
        # 4. Réduire les stocks via le port
        for item_data in items:
            product_id = item_data['product_id']
            quantity = item_data['quantity']
            
            product = self.product_repository.find_by_id(product_id)
            product.reduce_stock(quantity)
            self.product_repository.update(product)
        
        # 5. Sauvegarder la commande via le port
        saved_order = self.order_repository.save(order)
        
        # 6. Envoyer email de confirmation via le port
        # (asynchrone, non bloquant)
        try:
            self.email_service.send_order_confirmation(
                user_id=user_id,
                order_id=saved_order.id,
                total=saved_order.calculate_total().amount
            )
        except Exception as e:
            # Log mais ne fait pas échouer la commande
            print(f"Failed to send email: {e}")
        
        # 7. Retourner la commande
        return saved_order
```

---

**core/ports/secondary/repositories/order_repository.py**

```python
from abc import ABC, abstractmethod
from typing import List, Optional
from core.domain.entities.order import Order

class OrderRepository(ABC):
    """
    [PLUGIN] PORT SECONDAIRE : Interface pour persister les commandes
    
    Défini PAR le Core
    Implémenté PAR les adaptateurs secondaires
    
    Le Core ne connaît que cette interface
    Les implémentations (PostgreSQL, MongoDB, etc.) sont dans adapters/
    """
    
    @abstractmethod
    def save(self, order: Order) -> Order:
        """
        Sauvegarde une commande
        
        Args:
            order: Commande à sauvegarder
            
        Returns:
            Order: Commande sauvegardée (avec ID)
        """
        pass
    
    @abstractmethod
    def find_by_id(self, order_id: int) -> Optional[Order]:
        """
        Trouve une commande par ID
        
        Args:
            order_id: ID de la commande
            
        Returns:
            Optional[Order]: Commande ou None
        """
        pass
    
    @abstractmethod
    def find_by_user_id(self, user_id: int) -> List[Order]:
        """
        Trouve toutes les commandes d'un utilisateur
        
        Args:
            user_id: ID de l'utilisateur
            
        Returns:
            List[Order]: Liste des commandes
        """
        pass
    
    @abstractmethod
    def update(self, order: Order) -> Order:
        """
        Met à jour une commande
        
        Args:
            order: Commande à mettre à jour
            
        Returns:
            Order: Commande mise à jour
        """
        pass
    
    @abstractmethod
    def delete(self, order_id: int) -> bool:
        """
        Supprime une commande
        
        Args:
            order_id: ID de la commande
            
        Returns:
            bool: True si supprimé, False sinon
        """
        pass
```

---

### [OUTIL] ADAPTATEURS

**adapters/primary/web/controllers/order_controller.py**

```python
from flask import Blueprint, request, jsonify
from core.use_cases.order.create_order import CreateOrderUseCase
from core.use_cases.order.get_order import GetOrderUseCase
from core.domain.exceptions.domain_exceptions import *

order_bp = Blueprint('orders', __name__, url_prefix='/api/orders')

class OrderController:
    """
    [OUTIL] ADAPTATEUR PRIMAIRE : Controller Web (Flask)
    
    Adapte les requêtes HTTP pour appeler les Use Cases (ports primaires)
    Vit HORS du Core
    """
    
    def __init__(
        self,
        create_order_use_case: CreateOrderUseCase,
        get_order_use_case: GetOrderUseCase
    ):
        # Dépend des Use Cases (ports primaires)
        self.create_order_use_case = create_order_use_case
        self.get_order_use_case = get_order_use_case
    
    def create_order(self):
        """
        POST /api/orders
        
        Adapte HTTP -> Use Case
        """
        try:
            # 1. Extraire les données de la requête HTTP
            data = request.get_json()
            
            # 2. Valider les données (validation HTTP basique)
            if 'user_id' not in data:
                return jsonify({'error': 'user_id is required'}), 400
            
            if 'items' not in data or not data['items']:
                return jsonify({'error': 'items are required'}), 400
            
            # 3. Appeler le Use Case (port primaire)
            order = self.create_order_use_case.execute(
                user_id=data['user_id'],
                items=data['items']
            )
            
            # 4. Adapter la réponse : Entity -> JSON
            response = {
                'id': order.id,
                'user_id': order.user_id,
                'items': [
                    {
                        'product_id': item.product_id,
                        'product_name': item.product_name,
                        'price': item.price.amount,
                        'quantity': item.quantity
                    }
                    for item in order.items
                ],
                'subtotal': order.calculate_subtotal().amount,
                'shipping_cost': order.shipping_cost.amount,
                'discount': order.discount.amount,
                'total': order.calculate_total().amount,
                'status': order.status,
                'created_at': order.created_at.isoformat()
            }
            
            return jsonify(response), 201
            
        except ProductNotFoundError as e:
            return jsonify({'error': str(e)}), 404
        
        except InsufficientStockError as e:
            return jsonify({'error': str(e)}), 400
        
        except InvalidOrderError as e:
            return jsonify({'error': str(e)}), 400
        
        except Exception as e:
            # Log l'erreur
            print(f"Unexpected error: {e}")
            return jsonify({'error': 'Internal server error'}), 500
    
    def get_order(self, order_id: int):
        """
        GET /api/orders/<order_id>
        
        Adapte HTTP -> Use Case
        """
        try:
            # Appeler le Use Case
            order = self.get_order_use_case.execute(order_id)
            
            # Adapter la réponse
            response = {
                'id': order.id,
                'user_id': order.user_id,
                'total': order.calculate_total().amount,
                'status': order.status
            }
            
            return jsonify(response), 200
            
        except OrderNotFoundError as e:
            return jsonify({'error': str(e)}), 404
        
        except Exception as e:
            return jsonify({'error': 'Internal server error'}), 500

# Routes Flask
def register_routes(
    app,
    create_order_use_case: CreateOrderUseCase,
    get_order_use_case: GetOrderUseCase
):
    """Enregistre les routes avec l'app Flask"""
    
    controller = OrderController(
        create_order_use_case=create_order_use_case,
        get_order_use_case=get_order_use_case
    )
    
    @order_bp.route('/', methods=['POST'])
    def create_order():
        return controller.create_order()
    
    @order_bp.route('/<int:order_id>', methods=['GET'])
    def get_order(order_id):
        return controller.get_order(order_id)
    
    app.register_blueprint(order_bp)
```

---

**adapters/secondary/repositories/postgresql/order_repository_impl.py**

```python
from typing import List, Optional
from sqlalchemy.orm import Session
from core.domain.entities.order import Order, OrderItem
from core.domain.value_objects.money import Money
from core.ports.secondary.repositories.order_repository import OrderRepository
from adapters.secondary.repositories.postgresql.models import (
    OrderModel,
    OrderItemModel
)

class PostgreSQLOrderRepository(OrderRepository):
    """
    [OUTIL] ADAPTATEUR SECONDAIRE : Repository PostgreSQL
    
    Implémente le port OrderRepository
    Vit HORS du Core
    """
    
    def __init__(self, session: Session):
        self.session = session
    
    def save(self, order: Order) -> Order:
        """Implémentation PostgreSQL de save"""
        
        # Convertir Entity -> Model ORM
        order_model = OrderModel(
            user_id=order.user_id,
            status=order.status,
            created_at=order.created_at
        )
        
        # Convertir les items
        for item in order.items:
            item_model = OrderItemModel(
                product_id=item.product_id,
                product_name=item.product_name,
                price=item.price.amount,
                currency=item.price.currency,
                quantity=item.quantity
            )
            order_model.items.append(item_model)
        
        # Sauvegarder en DB
        self.session.add(order_model)
        self.session.commit()
        self.session.refresh(order_model)
        
        # Convertir Model -> Entity
        return self._model_to_entity(order_model)
    
    def find_by_id(self, order_id: int) -> Optional[Order]:
        """Implémentation PostgreSQL de find_by_id"""
        
        order_model = self.session.query(OrderModel)\
                                  .filter_by(id=order_id)\
                                  .first()
        
        if not order_model:
            return None
        
        return self._model_to_entity(order_model)
    
    def find_by_user_id(self, user_id: int) -> List[Order]:
        """Implémentation PostgreSQL de find_by_user_id"""
        
        order_models = self.session.query(OrderModel)\
                                   .filter_by(user_id=user_id)\
                                   .order_by(OrderModel.created_at.desc())\
                                   .all()
        
        return [self._model_to_entity(model) for model in order_models]
    
    def update(self, order: Order) -> Order:
        """Implémentation PostgreSQL de update"""
        
        order_model = self.session.query(OrderModel)\
                                  .filter_by(id=order.id)\
                                  .first()
        
        if not order_model:
            raise ValueError(f"Order {order.id} not found")
        
        order_model.status = order.status
        
        self.session.commit()
        self.session.refresh(order_model)
        
        return self._model_to_entity(order_model)
    
    def delete(self, order_id: int) -> bool:
        """Implémentation PostgreSQL de delete"""
        
        order_model = self.session.query(OrderModel)\
                                  .filter_by(id=order_id)\
                                  .first()
        
        if not order_model:
            return False
        
        self.session.delete(order_model)
        self.session.commit()
        
        return True
    
    def _model_to_entity(self, model: OrderModel) -> Order:
        """Convertit Model ORM -> Entity Domain"""
        
        # Convertir les items
        items = [
            OrderItem(
                product_id=item_model.product_id,
                product_name=item_model.product_name,
                price=Money(item_model.price, item_model.currency),
                quantity=item_model.quantity
            )
            for item_model in model.items
        ]
        
        # Créer l'entité
        return Order(
            order_id=model.id,
            user_id=model.user_id,
            items=items,
            status=model.status,
            created_at=model.created_at
        )
```

---

**adapters/secondary/repositories/in_memory/order_repository_impl.py**

```python
from typing import List, Optional, Dict
from core.domain.entities.order import Order
from core.ports.secondary.repositories.order_repository import OrderRepository

class InMemoryOrderRepository(OrderRepository):
    """
    [OUTIL] ADAPTATEUR SECONDAIRE : Repository In-Memory
    
    Implémente le même port OrderRepository
    Utilisé pour les tests (rapide, pas de DB)
    """
    
    def __init__(self):
        self._orders: Dict[int, Order] = {}
        self._next_id = 1
    
    def save(self, order: Order) -> Order:
        """Implémentation In-Memory de save"""
        
        # Générer un ID si nouveau
        if order.id is None:
            order.id = self._next_id
            self._next_id += 1
        
        # Stocker en mémoire
        self._orders[order.id] = order
        
        return order
    
    def find_by_id(self, order_id: int) -> Optional[Order]:
        """Implémentation In-Memory de find_by_id"""
        return self._orders.get(order_id)
    
    def find_by_user_id(self, user_id: int) -> List[Order]:
        """Implémentation In-Memory de find_by_user_id"""
        return [
            order for order in self._orders.values()
            if order.user_id == user_id
        ]
    
    def update(self, order: Order) -> Order:
        """Implémentation In-Memory de update"""
        if order.id not in self._orders:
            raise ValueError(f"Order {order.id} not found")
        
        self._orders[order.id] = order
        return order
    
    def delete(self, order_id: int) -> bool:
        """Implémentation In-Memory de delete"""
        if order_id in self._orders:
            del self._orders[order_id]
            return True
        return False
    
    def clear(self):
        """Méthode utilitaire pour les tests"""
        self._orders.clear()
        self._next_id = 1
```

**[OK] Même interface, 2 implémentations différentes !**

---

### [CONFIG] CONFIGURATION (Dependency Injection)

**config/dependency_injection.py**

```python
from flask import Flask
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

# Core
from core.use_cases.order.create_order import CreateOrderUseCase
from core.use_cases.order.get_order import GetOrderUseCase

# Adapters Secondary
from adapters.secondary.repositories.postgresql.order_repository_impl import (
    PostgreSQLOrderRepository
)
from adapters.secondary.repositories.postgresql.product_repository_impl import (
    PostgreSQLProductRepository
)
from adapters.secondary.services.smtp_email_service import SMTPEmailService

# Adapters Primary
from adapters.primary.web.controllers.order_controller import register_routes

def configure_dependencies(app: Flask, config: dict):
    """
    Configure l'injection de dépendances
    
    Câble les adaptateurs aux ports
    """
    
    # Database
    engine = create_engine(config['DATABASE_URL'])
    SessionLocal = sessionmaker(bind=engine)
    
    def get_session():
        session = SessionLocal()
        try:
            yield session
        finally:
            session.close()
    
    # Adaptateurs Secondaires (implémentations)
    session = next(get_session())
    
    order_repository = PostgreSQLOrderRepository(session)
    product_repository = PostgreSQLProductRepository(session)
    email_service = SMTPEmailService(
        host=config['SMTP_HOST'],
        port=config['SMTP_PORT'],
        username=config['SMTP_USER'],
        password=config['SMTP_PASSWORD']
    )
    
    # Use Cases (câblage des dépendances)
    create_order_use_case = CreateOrderUseCase(
        order_repository=order_repository,
        product_repository=product_repository,
        email_service=email_service
    )
    
    get_order_use_case = GetOrderUseCase(
        order_repository=order_repository
    )
    
    # Adaptateurs Primaires (enregistrement des routes)
    register_routes(
        app=app,
        create_order_use_case=create_order_use_case,
        get_order_use_case=get_order_use_case
    )
    
    return app
```

**app.py (Point d'entrée)**

```python
from flask import Flask
from config.dependency_injection import configure_dependencies

def create_app(config=None):
    """Factory pour créer l'application"""
    
    app = Flask(__name__)
    
    # Configuration
    if config is None:
        config = {
            'DATABASE_URL': 'postgresql://localhost/hexagonal_db',
            'SMTP_HOST': 'smtp.gmail.com',
            'SMTP_PORT': 587,
            'SMTP_USER': 'your_email@gmail.com',
            'SMTP_PASSWORD': 'your_password'
        }
    
    # Configurer les dépendances (DI)
    app = configure_dependencies(app, config)
    
    return app

if __name__ == '__main__':
    app = create_app()
    app.run(host='0.0.0.0', port=5000, debug=True)
```

---

## [TEST] TESTS EN ARCHITECTURE HEXAGONALE

### Tests du Core (ultra rapides)

```python
# tests/unit/domain/test_order.py
def test_order_calculate_total():
    """Test de l'entité Order (pur, sans dépendances)"""
    
    # Arrange
    items = [
        OrderItem(
            product_id=1,
            product_name='Product 1',
            price=Money(50, 'EUR'),
            quantity=2
        ),
        OrderItem(
            product_id=2,
            product_name='Product 2',
            price=Money(30, 'EUR'),
            quantity=1
        )
    ]
    
    order = Order(user_id=1, items=items)
    
    # Act
    total = order.calculate_total()
    
    # Assert
    assert total.amount == 130  # 100 + 30 (no shipping, no discount)
    
    # [OK] Test ultra rapide (microseconde)
    # [OK] Pas de DB, pas de framework, pas de HTTP

def test_order_shipping_cost_free_above_100():
    """Test règle métier : Livraison gratuite > 100€"""
    
    items = [
        OrderItem(1, 'Product', Money(120, 'EUR'), 1)
    ]
    
    order = Order(user_id=1, items=items)
    
    assert order.shipping_cost.amount == 0
    # [OK] Règle métier testée directement

def test_order_discount_10_percent_above_500():
    """Test règle métier : 10% de réduction > 500€"""
    
    items = [
        OrderItem(1, 'Product', Money(600, 'EUR'), 1)
    ]
    
    order = Order(user_id=1, items=items)
    
    assert order.discount.amount == 60  # 10% de 600
```

---

### Tests des Use Cases (avec mocks)

```python
# tests/unit/use_cases/test_create_order.py
from unittest.mock import Mock
import pytest

def test_create_order_success():
    """Test du Use Case avec mocks des ports"""
    
    # Arrange : Mocks des ports secondaires
    mock_order_repo = Mock(spec=OrderRepository)
    mock_product_repo = Mock(spec=ProductRepository)
    mock_email_service = Mock(spec=EmailService)
    
    # Configurer les mocks
    mock_product = Product(id=1, name='Product 1', price=50, stock=100)
    mock_product_repo.find_by_id.return_value = mock_product
    
    mock_saved_order = Order(
        order_id=1,
        user_id=1,
        items=[OrderItem(1, 'Product 1', Money(50, 'EUR'), 2)]
    )
    mock_order_repo.save.return_value = mock_saved_order
    
    # Use Case avec mocks
    use_case = CreateOrderUseCase(
        order_repository=mock_order_repo,
        product_repository=mock_product_repo,
        email_service=mock_email_service
    )
    
    # Act
    order = use_case.execute(
        user_id=1,
        items=[{'product_id': 1, 'quantity': 2}]
    )
    
    # Assert
    assert order.id == 1
    assert order.user_id == 1
    assert len(order.items) == 1
    
    # Vérifier les appels aux ports
    mock_product_repo.find_by_id.assert_called_once_with(1)
    mock_order_repo.save.assert_called_once()
    mock_email_service.send_order_confirmation.assert_called_once()
    
    # [OK] Test rapide (millisecondes)
    # [OK] Pas de vraie DB
    # [OK] Pas de vrai email

def test_create_order_insufficient_stock():
    """Test erreur : Stock insuffisant"""
    
    # Arrange
    mock_order_repo = Mock()
    mock_product_repo = Mock()
    mock_email_service = Mock()
    
    # Produit avec stock insuffisant
    mock_product = Product(id=1, name='Product 1', price=50, stock=1)
    mock_product_repo.find_by_id.return_value = mock_product
    
    use_case = CreateOrderUseCase(
        mock_order_repo,
        mock_product_repo,
        mock_email_service
    )
    
    # Act & Assert
    with pytest.raises(InsufficientStockError):
        use_case.execute(
            user_id=1,
            items=[{'product_id': 1, 'quantity': 10}]  # Demande 10, stock = 1
        )
    
    # Vérifier que save n'a PAS été appelé
    mock_order_repo.save.assert_not_called()
```

---

### Tests des Adaptateurs (intégration)

```python
# tests/integration/adapters/test_postgresql_order_repository.py
def test_postgresql_save_order():
    """Test de l'adaptateur PostgreSQL (intégration DB)"""
    
    # Arrange : DB de test
    test_engine = create_engine('postgresql://localhost/test_db')
    TestSession = sessionmaker(bind=test_engine)
    session = TestSession()
    
    # Repository avec vraie DB de test
    repository = PostgreSQLOrderRepository(session)
    
    # Créer une commande
    order = Order(
        user_id=1,
        items=[
            OrderItem(1, 'Product 1', Money(50, 'EUR'), 2)
        ]
    )
    
    # Act
    saved_order = repository.save(order)
    
    # Assert
    assert saved_order.id is not None
    
    # Vérifier en DB
    found_order = repository.find_by_id(saved_order.id)
    assert found_order is not None
    assert found_order.user_id == 1
    
    # [OK] Test d'intégration avec vraie DB
    # [OK] Isolé du reste de l'application

def test_in_memory_repository():
    """Test de l'adaptateur In-Memory (rapide)"""
    
    # Arrange
    repository = InMemoryOrderRepository()
    
    order = Order(
        user_id=1,
        items=[OrderItem(1, 'Product', Money(50, 'EUR'), 1)]
    )
    
    # Act
    saved_order = repository.save(order)
    found_order = repository.find_by_id(saved_order.id)
    
    # Assert
    assert found_order.id == saved_order.id
    assert found_order.user_id == 1
    
    # [OK] Test ultra rapide (pas de DB)
    # [OK] Même interface que PostgreSQL
```

---

## [OBJECTIF] COMPARAISON : HEXAGONALE VS CLEAN ARCHITECTURE

```
┌─────────────────────────────────────────────────────────────┐
│ SIMILITUDES (90% identiques)                                │
├─────────────────────────────────────────────────────────────┤
│ [OK] Logique métier au centre (indépendante)                  │
│ [OK] Dépendances pointent vers le centre                      │
│ [OK] Interfaces (ports) pour l'isolation                      │
│ [OK] Adaptateurs implémentent les interfaces                  │
│ [OK] Testabilité maximale                                     │
│ [OK] Indépendance des frameworks                              │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│ DIFFÉRENCES (terminologie principalement)                   │
├─────────────────────────────────────────────────────────────┤
│ Hexagonale              │ Clean Architecture                │
├─────────────────────────┼───────────────────────────────────┤
│ Ports & Adapters        │ Couches circulaires (4 cercles)  │
│ Symétrie (hexagone)     │ Hiérarchie (cercles concentriques)│
│ Ports primaires/second. │ Input/Output boundaries           │
│ Adaptateurs             │ Interface Adapters                │
│ Application Core        │ Use Cases + Entities              │
│ Emphasize: Symétrie     │ Emphasize: Dépendances            │
└─────────────────────────┴───────────────────────────────────┘

EN PRATIQUE : Presque identiques
On peut dire qu'Hexagonale = Clean Architecture avec une autre visualisation
```

---

## [BRAVO] CONCLUSION

### Ce que tu as appris

**Architecture Hexagonale :**
- [OK] Logique métier au centre (Core)
- [OK] Ports (interfaces) pour isoler
- [OK] Adaptateurs (implémentations) autour
- [OK] Ports primaires (inbound) vs secondaires (outbound)
- [OK] Symétrie totale
- [OK] Indépendance maximale

**Pratique :**
- [OK] Exemple e-commerce complet
- [OK] Core avec Use Cases et Entities
- [OK] Ports définis
- [OK] Adaptateurs multiples (PostgreSQL, In-Memory, Web, CLI)
- [OK] Tests à tous les niveaux
- [OK] Dependency Injection

---

### Points clés

```
[OBJECTIF] Hexagonale = Ports & Adapters
[OBJECTIF] Core au centre, 100% indépendant
[OBJECTIF] Ports = Interfaces (contrats)
[OBJECTIF] Adaptateurs = Implémentations (interchangeables)
[OBJECTIF] Symétrie : Pas de "haut" ou "bas"
[OBJECTIF] Testabilité maximale (Core sans dépendances)
[OBJECTIF] Très similaire à Clean Architecture
```

---

### Quand utiliser Hexagonale ?

```
[OK] OUI :
- Projet long terme (> 12 mois)
- Besoin de flexibilité technologique
- Tests critiques
- Plusieurs interfaces (Web + CLI + API)
- Logique métier complexe
- Équipe 3-10 développeurs

[X] NON :
- Prototypes rapides
- CRUD simple
- Pas de logique métier
- Équipe < 3 développeurs
- Projet < 6 mois
```

---

### Prochaines étapes

1. Code l'exemple e-commerce hexagonal
2. Compare avec Clean Architecture
3. Crée plusieurs adaptateurs pour le même port
4. Lis `architecture_event_driven.txt`

**[RAPIDE] Tu maîtrises maintenant l'Architecture Hexagonale ! [RAPIDE]**

═══════════════════════════════════════════════════════════════
FIN DU FICHIER : architecture_hexagonale.txt
═══════════════════════════════════════════════════════════════

# [CIRCUS_TENT] ARCHITECTURE EVENT-DRIVEN (PILOTÉE PAR LES ÉVÉNEMENTS) - GUIDE ULTRA DÉTAILLÉ

## * INTRODUCTION

### Qu'est-ce que l'architecture Event-Driven ?

**Architecture Event-Driven (EDA)** = Architecture où les composants communiquent via des **événements** plutôt que par appels directs. Les composants **publient** des événements et **s'abonnent** aux événements qui les intéressent.

**Analogie : Journal (Newspaper) [NEWSPAPER]**

```
[NEWSPAPER] JOURNAL TRADITIONNEL (Event-Driven)

┌─────────────────────────────────────────────────────────┐
│  ÉDITEUR (Publisher)                                    │
│  └─ Publie un article : "Nouveau restaurant ouvert !"  │
└────────────────────────┬────────────────────────────────┘
                         │
                         v (Événement publié)
┌─────────────────────────────────────────────────────────┐
│  JOURNAL / CANAL (Event Bus)                            │
│  └─ Distribue l'information                             │
└─┬───────────────────┬───────────────────┬───────────────┘
  │                   │                   │
  v                   v                   v
ABONNÉ 1         ABONNÉ 2           ABONNÉ 3
(Lecteur A)      (Lecteur B)        (Lecteur C)
Reçoit           Reçoit             Reçoit
l'info           l'info             l'info

Caractéristiques :
[OK] L'éditeur ne CONNAÎT PAS les lecteurs
[OK] Les lecteurs ne CONNAISSENT PAS l'éditeur
[OK] Nouveaux lecteurs peuvent s'abonner à tout moment
[OK] Communication asynchrone (le lecteur lit quand il veut)
```

---

**Comparaison : Communication directe vs Event-Driven**

```
[X] COMMUNICATION DIRECTE (Synchrone)

Service A ─[appel direct]-> Service B
          <-[attend réponse]─

Problèmes :
- A doit CONNAÎTRE B
- A est BLOQUÉ en attendant B
- Si B est down, A échoue
- Couplage fort

──────────────────────────────────────────────────────

[OK] EVENT-DRIVEN (Asynchrone)

Service A ──[publie événement]-> Event Bus
                                    │
                                    ├-> Service B (abonné)
                                    ├-> Service C (abonné)
                                    └-> Service D (abonné)

Avantages :
- A ne CONNAÎT PAS B, C, D
- A n'attend PAS de réponse
- Si B est down, C et D reçoivent quand même
- Couplage faible
```

---

### [GRAPHIQUE] Concepts clés

**1. Événement (Event) [ANNONCE]**

```
Un événement = Quelque chose qui S'EST PASSÉ

Exemples :
[OK] UserRegistered (Utilisateur inscrit)
[OK] OrderPlaced (Commande passée)
[OK] PaymentCompleted (Paiement effectué)
[OK] EmailSent (Email envoyé)
[OK] ProductOutOfStock (Produit en rupture)

Caractéristiques :
- Temps PASSÉ (past tense)
- Immutable (ne change pas)
- Contient les données de l'événement
- Horodaté
```

**Structure d'un événement :**

```python
{
    "event_id": "evt_123456",
    "event_type": "OrderPlaced",
    "timestamp": "2024-01-15T10:30:00Z",
    "version": "1.0",
    "data": {
        "order_id": 42,
        "user_id": 7,
        "total": 150.00,
        "items": [
            {"product_id": 1, "quantity": 2},
            {"product_id": 3, "quantity": 1}
        ]
    },
    "metadata": {
        "source": "order-service",
        "correlation_id": "cor_789"
    }
}
```

---

**2. Publisher (Éditeur) [SORTIE]**

```
Publisher = Composant qui PUBLIE des événements

Responsabilités :
[OK] Créer l'événement
[OK] Le publier sur l'Event Bus
[OK] NE PAS savoir qui va le recevoir
[OK] NE PAS attendre de réponse

Exemple :
class OrderService:
    def create_order(self, user_id, items):
        # Créer la commande
        order = Order.create(user_id, items)
        
        # Publier l'événement
        event = OrderPlacedEvent(
            order_id=order.id,
            user_id=user_id,
            total=order.total
        )
        
        event_bus.publish(event)
        
        return order
```

---

**3. Subscriber (Abonné) [ENTREE]**

```
Subscriber = Composant qui S'ABONNE à des événements

Responsabilités :
[OK] Écouter les événements qui l'intéressent
[OK] Réagir à l'événement
[OK] NE PAS savoir qui a publié
[OK] Traiter de manière asynchrone

Exemple :
class EmailService:
    @subscribe('OrderPlaced')
    def on_order_placed(self, event):
        # Réagir à l'événement
        user = self.user_repo.find(event.data['user_id'])
        
        self.send_email(
            to=user.email,
            subject="Confirmation de commande",
            body=f"Votre commande #{event.data['order_id']} est confirmée"
        )

class InventoryService:
    @subscribe('OrderPlaced')
    def on_order_placed(self, event):
        # Réagir au même événement
        for item in event.data['items']:
            self.reduce_stock(item['product_id'], item['quantity'])
```

---

**4. Event Bus (Bus d'événements) [TRANSPORT]**

```
Event Bus = Infrastructure qui TRANSPORTE les événements

Responsabilités :
[OK] Recevoir les événements publiés
[OK] Router vers les abonnés
[OK] Garantir la livraison
[OK] Gérer la persistance (optionnel)

Technologies populaires :
- RabbitMQ
- Apache Kafka
- AWS EventBridge
- Redis Pub/Sub
- Google Cloud Pub/Sub
- Azure Event Hubs
```

---

### [OBJECTIF] Patterns Event-Driven

**1. Pub/Sub (Publish/Subscribe) [ANNONCE]**

```
Le plus basique

Publisher ──[publie]-> Topic/Channel ──[distribue]-> Subscribers

Exemple :
OrderService publie "OrderPlaced"
    v
Topic "orders"
    v
    ├-> EmailService (envoie email)
    ├-> InventoryService (réduit stock)
    └-> AnalyticsService (enregistre stats)

Caractéristiques :
[OK] 1 publisher, N subscribers
[OK] Tous les subscribers reçoivent l'événement
[OK] Fire-and-forget (publie et oublie)
```

---

**2. Event Streaming [WATER_WAVE]**

```
Événements stockés dans un log ordonné

Producer ──[écrit]-> Event Stream (log) ──[lit]-> Consumers
                     (Kafka, etc.)

Exemple avec Kafka :
OrderService écrit dans topic "orders"
    v
Kafka Stream (log persisté)
    v
    ├-> EmailService lit depuis offset 0
    ├-> AnalyticsService lit depuis offset 1000
    └-> BackupService lit depuis offset 0 (replay)

Caractéristiques :
[OK] Événements PERSISTÉS
[OK] Réutilisation possible (replay)
[OK] Ordre garanti
[OK] Scalabilité massive
```

---

**3. Event Sourcing [DOC]**

```
L'état est DÉRIVÉ des événements

État actuel = Somme de tous les événements

Exemple :
Compte bancaire : Solde = 1000€

Comment on arrive à 1000€ ?
├─ AccountCreated(balance=0)       -> Solde: 0€
├─ MoneyDeposited(amount=500)      -> Solde: 500€
├─ MoneyDeposited(amount=700)      -> Solde: 1200€
└─ MoneyWithdrawn(amount=200)      -> Solde: 1000€

On ne stocke PAS le solde directement
On stocke les ÉVÉNEMENTS
Le solde est RECALCULÉ à partir des événements

Avantages :
[OK] Historique complet
[OK] Audit trail
[OK] Possibilité de "rejouer" les événements
[OK] Pas de perte d'information
```

---

**4. CQRS (Command Query Responsibility Segregation) [COMBAT]**

```
Séparation des ÉCRITURES et des LECTURES

         ┌──────────────┐
Commande │    WRITE     │ Événements
(Write)  │    MODEL     │ ─────────-> Event Store
         └──────────────┘              │
                                       │
                                       v (projection)
         ┌──────────────┐              │
Requête  │    READ      │ <-────────────┘
(Read)   │    MODEL     │
         └──────────────┘

Exemple :
WRITE MODEL (optimisé pour écriture) :
- Valider et écrire les commandes
- Publier les événements
- Base de données normalisée

READ MODEL (optimisé pour lecture) :
- Données dénormalisées
- Vues matérialisées
- Cache agressif
- ElasticSearch pour recherche

Avantages :
[OK] Scalabilité indépendante (read vs write)
[OK] Optimisation séparée
[OK] Complexité répartie
```

---

## [REFLEXION] POURQUOI UTILISER L'ARCHITECTURE EVENT-DRIVEN ?

### [OK] Avantages

**1. Découplage total [DEVERROUILLE]**

**Sans Event-Driven (couplage) :**

```python
# [X] Service Order couplé à EmailService et InventoryService
class OrderService:
    def __init__(self, email_service, inventory_service):
        self.email_service = email_service
        self.inventory_service = inventory_service
    
    def create_order(self, user_id, items):
        # Créer la commande
        order = Order.create(user_id, items)
        
        # Appeler directement EmailService
        self.email_service.send_confirmation(order)
        
        # Appeler directement InventoryService
        self.inventory_service.reduce_stock(items)
        
        return order

Problèmes :
[X] OrderService CONNAÎT EmailService et InventoryService
[X] Si EmailService est down -> OrderService échoue
[X] Ajouter un nouveau service -> Modifier OrderService
[X] Tests difficiles (dépendances multiples)
```

---

**Avec Event-Driven (découplage) :**

```python
# [OK] Service Order découplé
class OrderService:
    def __init__(self, event_bus):
        self.event_bus = event_bus
    
    def create_order(self, user_id, items):
        # Créer la commande
        order = Order.create(user_id, items)
        
        # Publier l'événement (et c'est tout !)
        event = OrderPlacedEvent(
            order_id=order.id,
            user_id=user_id,
            total=order.total,
            items=items
        )
        
        self.event_bus.publish(event)
        
        return order

# [OK] EmailService écoute l'événement (indépendamment)
class EmailService:
    @subscribe('OrderPlaced')
    def on_order_placed(self, event):
        user = self.get_user(event.user_id)
        self.send_email(user.email, "Confirmation", ...)

# [OK] InventoryService écoute aussi (indépendamment)
class InventoryService:
    @subscribe('OrderPlaced')
    def on_order_placed(self, event):
        for item in event.items:
            self.reduce_stock(item['product_id'], item['quantity'])

# [OK] Nouveau service ? Juste s'abonner (pas de modification d'OrderService)
class AnalyticsService:
    @subscribe('OrderPlaced')
    def on_order_placed(self, event):
        self.track_sale(event.total)

Avantages :
[OK] OrderService ne CONNAÎT PAS les autres services
[OK] EmailService et InventoryService indépendants
[OK] Si EmailService est down -> OrderService continue
[OK] Ajouter AnalyticsService -> ZÉRO modification d'OrderService
[OK] Tests faciles (juste vérifier que l'événement est publié)
```

---

**2. Scalabilité et résilience [HAUSSE]**

**Scénario : Pic de trafic Black Friday**

```
Sans Event-Driven (synchrone) :

Client -> OrderService -> EmailService (30 sec pour envoyer)
                     v
              InventoryService (5 sec pour réduire stock)
                     v
              PaymentService (10 sec pour payer)

Temps total : 45 secondes par commande
Avec 1000 commandes/min -> IMPOSSIBLE [X]
```

---

```
Avec Event-Driven (asynchrone) :

Client -> OrderService (0.5 sec)
            v [publie OrderPlaced]
            Event Bus
            v
            ├-> EmailService (traite quand il peut)
            ├-> InventoryService (traite quand il peut)
            └-> PaymentService (traite quand il peut)

Temps de réponse utilisateur : 0.5 seconde
Les autres services traitent en arrière-plan

Avec 1000 commandes/min -> OK [OK]

Scalabilité :
- EmailService lent ? -> Ajouter plus d'instances EmailService
- InventoryService rapide ? -> Garder 1 seule instance
- Scalabilité INDÉPENDANTE par service
```

---

**Résilience :**

```
Si EmailService est down :

[X] Sans Event-Driven :
OrderService échoue -> Client reçoit une erreur

[OK] Avec Event-Driven :
OrderService réussit -> Client reçoit confirmation
EmailService traitera l'événement quand il sera de retour
(Les événements sont mis en queue)

Avantages :
[OK] Dégradation gracieuse
[OK] Pas de perte de données
[OK] Réessai automatique
```

---

**3. Extensibilité facile [PLUGIN]**

**Ajouter une fonctionnalité SANS modifier le code existant :**

```python
# Code existant (ne change PAS)
class OrderService:
    def create_order(self, user_id, items):
        order = Order.create(user_id, items)
        
        # Publie l'événement
        self.event_bus.publish(OrderPlacedEvent(order))
        
        return order

# ────────────────────────────────────────────────────

# Nouvelle fonctionnalité : Programme de fidélité
# On ajoute JUSTE un nouveau subscriber
class LoyaltyService:
    @subscribe('OrderPlaced')
    def on_order_placed(self, event):
        # Ajouter des points de fidélité
        points = int(event.total / 10)  # 1 point par 10€
        self.loyalty_repo.add_points(event.user_id, points)

# ────────────────────────────────────────────────────

# Nouvelle fonctionnalité : Recommandations
class RecommendationService:
    @subscribe('OrderPlaced')
    def on_order_placed(self, event):
        # Mettre à jour les recommandations
        purchased_products = [item['product_id'] for item in event.items]
        self.update_recommendations(event.user_id, purchased_products)

# ────────────────────────────────────────────────────

# Nouvelle fonctionnalité : Notifications push
class PushNotificationService:
    @subscribe('OrderPlaced')
    def on_order_placed(self, event):
        # Envoyer une notification push
        self.send_push(event.user_id, "Commande confirmée !")

# [OK] OrderService n'a PAS changé
# [OK] Principe Open/Closed (SOLID)
# [OK] Pas de régression possible
```

---

**4. Audit et traçabilité [LISTE]**

**Avec Event Sourcing :**

```
Tous les événements sont PERSISTÉS

Commande #42 :
├─ 2024-01-15 10:30:00 - OrderPlaced(order_id=42, total=150)
├─ 2024-01-15 10:31:23 - PaymentCompleted(order_id=42, amount=150)
├─ 2024-01-15 11:15:45 - OrderShipped(order_id=42, tracking="TRACK123")
└─ 2024-01-17 14:22:10 - OrderDelivered(order_id=42)

Avantages :
[OK] Historique COMPLET de la commande
[OK] Qui a fait quoi et quand
[OK] Impossible de perdre des données
[OK] Audit trail pour compliance
[OK] Debugging facilité (rejouer les événements)
[OK] Analytics (analyser les patterns)

Exemple de requête :
"Combien de commandes sont payées mais pas encore expédiées ?"
-> Filtrer les OrderPlaced + PaymentCompleted - OrderShipped
```

---

**5. Évolution temporelle (Time Travel) [ALARM_CLOCK]**

**Rejouer l'historique :**

```python
# Reconstruire l'état à une date donnée
def get_order_state_at(order_id, date):
    """Obtenir l'état de la commande à une date passée"""
    
    # Récupérer tous les événements jusqu'à cette date
    events = event_store.get_events(
        aggregate_id=order_id,
        until=date
    )
    
    # Rejouer les événements
    order = Order()
    for event in events:
        order.apply(event)
    
    return order

# Utilisation
# État de la commande le 15 janvier à 10h30
order_state = get_order_state_at(42, datetime(2024, 1, 15, 10, 30))
print(f"Status: {order_state.status}")  # "pending"

# État de la commande le 17 janvier à 15h
order_state = get_order_state_at(42, datetime(2024, 1, 17, 15, 0))
print(f"Status: {order_state.status}")  # "delivered"

# [OK] Machine à remonter le temps !
```

---

### [X] Inconvénients

**1. Complexité accrue [SHOCKED_FACE_WITH_EXPLODING_HEAD]**

**Event-Driven ajoute de la complexité :**

```
Sans Event-Driven (simple) :
Service A -> Service B (appel direct)
            v
         Réponse

[OK] Simple
[OK] Facile à débugger
[OK] Flux clair

──────────────────────────────────────────────────

Avec Event-Driven (complexe) :
Service A -> Event Bus -> Service B
                      -> Service C
                      -> Service D

[X] Flux difficile à suivre
[X] Où est l'erreur ?
[X] Qui a consommé l'événement ?
[X] Dans quel ordre ?
[X] Debugging difficile

Nouveaux problèmes :
- Eventual consistency (cohérence éventuelle)
- Event ordering (ordre des événements)
- Duplicate events (événements dupliqués)
- Failed consumers (consommateurs échoués)
- Message replay (rejouer les messages)
```

---

**2. Eventual Consistency (Cohérence éventuelle) [HOURGLASS_WITH_FLOWING_SAND]**

**Problème : Les données ne sont pas immédiatement cohérentes**

```
Scénario :

T0 : User crée une commande
     OrderService publie OrderPlaced
     
T0+1ms : Client rafraîchit la page
         EmailService n'a PAS encore traité l'événement
         -> Client ne voit pas d'email de confirmation
         -> Client pense que ça a échoué [FACE_WITH_OPEN_MOUTH_AND_COLD_SWEAT]

T0+500ms : EmailService traite l'événement
           Email envoyé
           -> Client reçoit l'email
           -> Maintenant cohérent [OK]

Problème : Délai entre l'action et la cohérence
```

**Solution : Informer l'utilisateur**

```
[X] Mauvais UX :
"Commande créée !"
(Utilisateur attend l'email, ne le reçoit pas tout de suite, panique)

[OK] Bon UX :
"Commande créée ! Vous recevrez un email de confirmation dans quelques instants."
(Utilisateur sait qu'il faut attendre)
```

---

**3. Debugging difficile [BUG]**

**Tracer le flux d'un événement :**

```
Problème : Une commande n'a pas reçu d'email

Où est le problème ?
1. OrderService a-t-il publié l'événement ? [REFLEXION]
2. Event Bus a-t-il reçu l'événement ? [REFLEXION]
3. EmailService s'est-il abonné correctement ? [REFLEXION]
4. EmailService a-t-il traité l'événement ? [REFLEXION]
5. L'email a-t-il été envoyé ? [REFLEXION]
6. L'email est-il arrivé dans la boîte ? [REFLEXION]

Dans une architecture synchrone :
OrderService.create_order() échoue -> Erreur claire

Dans une architecture asynchrone :
OrderService.create_order() réussit -> Mais où est l'email ? [CONFUSED_FACE]

Solutions :
[OK] Correlation IDs (suivre un événement à travers le système)
[OK] Distributed Tracing (Jaeger, Zipkin)
[OK] Logging centralisé (ELK Stack)
[OK] Event monitoring (Kafka UI, RabbitMQ Management)
```

---

**4. Duplication d'événements [EMAIL][EMAIL]**

**Problème : Un événement peut être livré plusieurs fois**

```
Scénario :

OrderService publie OrderPlaced
    v
Event Bus reçoit
    v
EmailService consomme et traite
    v
EmailService échoue APRÈS avoir envoyé l'email (crash)
    v
Event Bus ne reçoit PAS l'ACK (acknowledgment)
    v
Event Bus RE-LIVRE l'événement
    v
EmailService traite À NOUVEAU
    v
Client reçoit 2 emails ! [!]

Solution : Idempotence
```

**Idempotence (traiter plusieurs fois = même résultat) :**

```python
class EmailService:
    @subscribe('OrderPlaced')
    def on_order_placed(self, event):
        # Vérifier si déjà traité
        if self.already_processed(event.event_id):
            return  # Ignorer (idempotence)
        
        # Envoyer l'email
        self.send_email(...)
        
        # Marquer comme traité
        self.mark_as_processed(event.event_id)

# [OK] Si l'événement arrive 2 fois, l'email n'est envoyé qu'une fois
```

---

**5. Ordre des événements [NOMBRE]**

**Problème : Les événements peuvent arriver dans le désordre**

```
Séquence attendue :
1. OrderPlaced
2. PaymentCompleted
3. OrderShipped

Séquence réelle (network issues, etc.) :
1. OrderPlaced
2. OrderShipped (arrive avant PaymentCompleted !)
3. PaymentCompleted

[IMPACT] Incohérence : Comment un order peut être shipped avant d'être payé ?

Solutions :
[OK] Event versioning (version dans l'événement)
[OK] Sequence numbers (numéro de séquence)
[OK] Kafka partitions (garantit l'ordre par partition)
[OK] Event causality (lien de causalité)
```

---

**6. Coût opérationnel [ARGENT]**

**Infrastructure supplémentaire requise :**

```
Sans Event-Driven :
├─ Application servers
└─ Database

Coût : ~100€/mois

──────────────────────────────────────────────────

Avec Event-Driven :
├─ Application servers
├─ Database
├─ Message Queue (RabbitMQ, Kafka)
│  ├─ 3 brokers (HA)
│  └─ Monitoring
├─ Event Store (si Event Sourcing)
├─ Distributed Tracing (Jaeger)
└─ Logging centralisé (ELK)

Coût : ~500€/mois

[ARGENT] 5x plus cher
```

---

## [CONSTRUCTION] ARCHITECTURE COMPLÈTE EVENT-DRIVEN

### [DOSSIER] Structure de projet

```
event_driven_app/
│
├── domain/                          # Domaine métier
│   ├── __init__.py
│   ├── entities/
│   │   ├── __init__.py
│   │   ├── order.py
│   │   ├── user.py
│   │   └── product.py
│   │
│   └── events/                      # [ANNONCE] Événements du domaine
│       ├── __init__.py
│       ├── base_event.py
│       ├── order_events.py          # OrderPlaced, OrderShipped, etc.
│       ├── user_events.py
│       └── payment_events.py
│
├── application/                     # Logique applicative
│   ├── __init__.py
│   ├── commands/                    # [NOTE] Commandes (Write)
│   │   ├── __init__.py
│   │   ├── create_order_command.py
│   │   └── cancel_order_command.py
│   │
│   ├── queries/                     # [RECHERCHE] Requêtes (Read)
│   │   ├── __init__.py
│   │   ├── get_order_query.py
│   │   └── list_orders_query.py
│   │
│   └── handlers/                    # [OBJECTIF] Command/Event Handlers
│       ├── __init__.py
│       ├── command_handlers/
│       │   └── order_command_handler.py
│       │
│       └── event_handlers/          # [ENTREE] Event Subscribers
│           ├─ email_event_handler.py
│           ├─ inventory_event_handler.py
│           └─ analytics_event_handler.py
│
├── infrastructure/                  # Infrastructure technique
│   ├── __init__.py
│   │
│   ├── event_bus/                   # [TRANSPORT] Event Bus
│   │   ├── __init__.py
│   │   ├── event_bus_interface.py
│   │   ├── rabbitmq_event_bus.py
│   │   ├── kafka_event_bus.py
│   │   └── in_memory_event_bus.py   # Pour tests
│   │
│   ├── event_store/                 # [DOC] Event Store
│   │   ├── __init__.py
│   │   ├── event_store_interface.py
│   │   ├── postgresql_event_store.py
│   │   └── in_memory_event_store.py
│   │
│   ├── repositories/                # Repositories
│   │   ├── __init__.py
│   │   ├── order_repository.py
│   │   └── user_repository.py
│   │
│   └── message_queue/               # Message Queue config
│       ├── __init__.py
│       └── rabbitmq_config.py
│
├── api/                             # API (Interface externe)
│   ├── __init__.py
│   ├── rest/
│   │   ├── __init__.py
│   │   ├── app.py
│   │   └── controllers/
│   │       └── order_controller.py
│   │
│   └── graphql/
│       └── schema.py
│
├── config/                          # Configuration
│   ├── __init__.py
│   ├── settings.py
│   └── dependency_injection.py
│
├── tests/                           # Tests
│   ├── unit/
│   ├── integration/
│   └── e2e/
│
├── docker-compose.yml               # Services (RabbitMQ, Kafka, etc.)
├── requirements.txt
└── README.md
```

---

## [CODE] CODE COMPLET - EXEMPLE E-COMMERCE

### [ANNONCE] ÉVÉNEMENTS

**domain/events/base_event.py**

```python
from datetime import datetime
from dataclasses import dataclass
from typing import Dict, Any
import uuid

@dataclass
class BaseEvent:
    """
    Base pour tous les événements
    
    Caractéristiques :
    - Immutable (frozen)
    - Horodaté
    - ID unique
    - Type d'événement
    """
    
    event_id: str
    event_type: str
    timestamp: datetime
    version: str
    data: Dict[str, Any]
    metadata: Dict[str, Any]
    
    @classmethod
    def create(cls, event_type: str, data: Dict[str, Any], 
               metadata: Dict[str, Any] = None):
        """Factory pour créer un événement"""
        return cls(
            event_id=str(uuid.uuid4()),
            event_type=event_type,
            timestamp=datetime.utcnow(),
            version="1.0",
            data=data,
            metadata=metadata or {}
        )
    
    def to_dict(self) -> Dict[str, Any]:
        """Convertit en dictionnaire pour sérialisation"""
        return {
            'event_id': self.event_id,
            'event_type': self.event_type,
            'timestamp': self.timestamp.isoformat(),
            'version': self.version,
            'data': self.data,
            'metadata': self.metadata
        }
    
    @classmethod
    def from_dict(cls, data: Dict[str, Any]):
        """Désérialise depuis un dictionnaire"""
        return cls(
            event_id=data['event_id'],
            event_type=data['event_type'],
            timestamp=datetime.fromisoformat(data['timestamp']),
            version=data['version'],
            data=data['data'],
            metadata=data.get('metadata', {})
        )
```

---

**domain/events/order_events.py**

```python
from dataclasses import dataclass
from typing import List, Dict
from domain.events.base_event import BaseEvent

@dataclass(frozen=True)
class OrderPlacedEvent(BaseEvent):
    """
    Événement : Une commande a été passée
    
    Publié par : OrderService
    Écouté par : EmailService, InventoryService, AnalyticsService
    """
    
    @classmethod
    def create(cls, order_id: int, user_id: int, items: List[Dict], 
               total: float, correlation_id: str = None):
        """Créer un événement OrderPlaced"""
        
        return super().create(
            event_type='OrderPlaced',
            data={
                'order_id': order_id,
                'user_id': user_id,
                'items': items,
                'total': total
            },
            metadata={
                'source': 'order-service',
                'correlation_id': correlation_id
            }
        )

@dataclass(frozen=True)
class OrderShippedEvent(BaseEvent):
    """
    Événement : Une commande a été expédiée
    
    Publié par : ShippingService
    Écouté par : EmailService, TrackingService
    """
    
    @classmethod
    def create(cls, order_id: int, tracking_number: str, 
               carrier: str, correlation_id: str = None):
        return super().create(
            event_type='OrderShipped',
            data={
                'order_id': order_id,
                'tracking_number': tracking_number,
                'carrier': carrier
            },
            metadata={
                'source': 'shipping-service',
                'correlation_id': correlation_id
            }
        )

@dataclass(frozen=True)
class OrderCancelledEvent(BaseEvent):
    """
    Événement : Une commande a été annulée
    
    Publié par : OrderService
    Écouté par : EmailService, InventoryService (restore stock), PaymentService (refund)
    """
    
    @classmethod
    def create(cls, order_id: int, user_id: int, reason: str, 
               correlation_id: str = None):
        return super().create(
            event_type='OrderCancelled',
            data={
                'order_id': order_id,
                'user_id': user_id,
                'reason': reason
            },
            metadata={
                'source': 'order-service',
                'correlation_id': correlation_id
            }
        )

@dataclass(frozen=True)
class PaymentCompletedEvent(BaseEvent):
    """
    Événement : Un paiement a été complété
    
    Publié par : PaymentService
    Écouté par : OrderService (confirmer), EmailService
    """
    
    @classmethod
    def create(cls, order_id: int, payment_id: str, 
               amount: float, method: str, correlation_id: str = None):
        return super().create(
            event_type='PaymentCompleted',
            data={
                'order_id': order_id,
                'payment_id': payment_id,
                'amount': amount,
                'method': method
            },
            metadata={
                'source': 'payment-service',
                'correlation_id': correlation_id
            }
        )
```

---

### [TRANSPORT] EVENT BUS

**infrastructure/event_bus/event_bus_interface.py**

```python
from abc import ABC, abstractmethod
from typing import Callable, List
from domain.events.base_event import BaseEvent

class EventBus(ABC):
    """
    Interface pour l'Event Bus
    
    Responsabilités :
    - Publier des événements
    - Abonner des handlers
    - Router les événements vers les abonnés
    """
    
    @abstractmethod
    def publish(self, event: BaseEvent):
        """
        Publier un événement
        
        Args:
            event: Événement à publier
        """
        pass
    
    @abstractmethod
    def subscribe(self, event_type: str, handler: Callable[[BaseEvent], None]):
        """
        S'abonner à un type d'événement
        
        Args:
            event_type: Type d'événement (ex: "OrderPlaced")
            handler: Fonction à appeler quand l'événement arrive
        """
        pass
    
    @abstractmethod
    def unsubscribe(self, event_type: str, handler: Callable[[BaseEvent], None]):
        """
        Se désabonner d'un type d'événement
        
        Args:
            event_type: Type d'événement
            handler: Handler à retirer
        """
        pass
```

---

**infrastructure/event_bus/rabbitmq_event_bus.py**

```python
import pika
import json
import threading
from typing import Callable, Dict, List
from domain.events.base_event import BaseEvent
from infrastructure.event_bus.event_bus_interface import EventBus

class RabbitMQEventBus(EventBus):
    """
    Implémentation Event Bus avec RabbitMQ
    
    Pattern : Pub/Sub avec Topics
    """
    
    def __init__(self, host: str = 'localhost', port: int = 5672):
        self.host = host
        self.port = port
        
        # Subscribers : {event_type: [handler1, handler2, ...]}
        self.subscribers: Dict[str, List[Callable]] = {}
        
        # Connection pour publishing
        self.publish_connection = pika.BlockingConnection(
            pika.ConnectionParameters(host=self.host, port=self.port)
        )
        self.publish_channel = self.publish_connection.channel()
        
        # Déclarer l'exchange
        self.publish_channel.exchange_declare(
            exchange='events',
            exchange_type='topic',
            durable=True
        )
    
    def publish(self, event: BaseEvent):
        """
        Publier un événement sur RabbitMQ
        
        Routing key = event_type (ex: "OrderPlaced")
        """
        
        # Sérialiser l'événement
        message = json.dumps(event.to_dict())
        
        # Publier
        self.publish_channel.basic_publish(
            exchange='events',
            routing_key=event.event_type,  # Routing key
            body=message,
            properties=pika.BasicProperties(
                delivery_mode=2,  # Persistent
                content_type='application/json'
            )
        )
        
        print(f"[SORTIE] Published: {event.event_type} (ID: {event.event_id})")
    
    def subscribe(self, event_type: str, handler: Callable[[BaseEvent], None]):
        """
        S'abonner à un type d'événement
        
        Crée une queue dédiée et la bind à l'exchange
        """
        
        # Enregistrer le handler
        if event_type not in self.subscribers:
            self.subscribers[event_type] = []
        
        self.subscribers[event_type].append(handler)
        
        # Démarrer un consumer dans un thread séparé
        consumer_thread = threading.Thread(
            target=self._start_consumer,
            args=(event_type, handler),
            daemon=True
        )
        consumer_thread.start()
        
        print(f"[ENTREE] Subscribed to: {event_type}")
    
    def _start_consumer(self, event_type: str, handler: Callable):
        """
        Démarre un consumer RabbitMQ pour un type d'événement
        
        Runs dans un thread séparé
        """
        
        # Nouvelle connection pour ce consumer
        connection = pika.BlockingConnection(
            pika.ConnectionParameters(host=self.host, port=self.port)
        )
        channel = connection.channel()
        
        # Déclarer l'exchange
        channel.exchange_declare(
            exchange='events',
            exchange_type='topic',
            durable=True
        )
        
        # Créer une queue dédiée
        queue_name = f"{event_type}.{handler.__name__}"
        result = channel.queue_declare(queue=queue_name, durable=True)
        
        # Bind la queue à l'exchange avec routing key = event_type
        channel.queue_bind(
            exchange='events',
            queue=queue_name,
            routing_key=event_type
        )
        
        def callback(ch, method, properties, body):
            """Callback appelé quand un message arrive"""
            try:
                # Désérialiser
                event_data = json.loads(body)
                event = BaseEvent.from_dict(event_data)
                
                print(f"[ENTREE] Received: {event.event_type} (ID: {event.event_id})")
                
                # Appeler le handler
                handler(event)
                
                # ACK (acknowledge)
                ch.basic_ack(delivery_tag=method.delivery_tag)
                
            except Exception as e:
                print(f"[X] Error handling event: {e}")
                # NACK (negative acknowledge) - remettre en queue
                ch.basic_nack(delivery_tag=method.delivery_tag, requeue=True)
        
        # Consommer
        channel.basic_qos(prefetch_count=1)  # Traiter 1 message à la fois
        channel.basic_consume(queue=queue_name, on_message_callback=callback)
        
        print(f"[AUDIO] Listening for: {event_type}")
        channel.start_consuming()
    
    def unsubscribe(self, event_type: str, handler: Callable):
        """Se désabonner (simplifié)"""
        if event_type in self.subscribers:
            self.subscribers[event_type].remove(handler)
    
    def close(self):
        """Fermer les connections"""
        self.publish_connection.close()
```

---

**infrastructure/event_bus/in_memory_event_bus.py**

```python
from typing import Callable, Dict, List
from domain.events.base_event import BaseEvent
from infrastructure.event_bus.event_bus_interface import EventBus

class InMemoryEventBus(EventBus):
    """
    Event Bus en mémoire (pour tests)
    
    Implémentation simple sans RabbitMQ/Kafka
    """
    
    def __init__(self):
        # Subscribers : {event_type: [handler1, handler2, ...]}
        self.subscribers: Dict[str, List[Callable]] = {}
        
        # Historique des événements publiés (pour tests)
        self.published_events: List[BaseEvent] = []
    
    def publish(self, event: BaseEvent):
        """
        Publier un événement
        
        Appelle IMMÉDIATEMENT tous les handlers (synchrone)
        """
        
        # Sauvegarder dans l'historique
        self.published_events.append(event)
        
        print(f"[SORTIE] Published: {event.event_type} (ID: {event.event_id})")
        
        # Trouver les handlers pour ce type d'événement
        handlers = self.subscribers.get(event.event_type, [])
        
        # Appeler chaque handler
        for handler in handlers:
            try:
                print(f"[ENTREE] Calling handler: {handler.__name__}")
                handler(event)
            except Exception as e:
                print(f"[X] Error in handler {handler.__name__}: {e}")
    
    def subscribe(self, event_type: str, handler: Callable[[BaseEvent], None]):
        """S'abonner à un type d'événement"""
        
        if event_type not in self.subscribers:
            self.subscribers[event_type] = []
        
        self.subscribers[event_type].append(handler)
        
        print(f"[ENTREE] Subscribed to: {event_type}")
    
    def unsubscribe(self, event_type: str, handler: Callable):
        """Se désabonner"""
        if event_type in self.subscribers:
            self.subscribers[event_type].remove(handler)
    
    def clear(self):
        """Nettoyer (pour tests)"""
        self.published_events.clear()
        self.subscribers.clear()
```

---

### [NOTE] COMMAND HANDLERS

**application/handlers/command_handlers/order_command_handler.py**

```python
from domain.entities.order import Order, OrderItem
from domain.events.order_events import OrderPlacedEvent, OrderCancelledEvent
from infrastructure.event_bus.event_bus_interface import EventBus
from infrastructure.repositories.order_repository import OrderRepository
from infrastructure.repositories.product_repository import ProductRepository

class CreateOrderCommand:
    """Commande : Créer une commande"""
    
    def __init__(self, user_id: int, items: List[Dict]):
        self.user_id = user_id
        self.items = items  # [{'product_id': 1, 'quantity': 2}, ...]

class OrderCommandHandler:
    """
    Handler pour les commandes Order
    
    Responsabilités :
    - Exécuter les commandes (Write)
    - Publier les événements correspondants
    """
    
    def __init__(
        self,
        order_repository: OrderRepository,
        product_repository: ProductRepository,
        event_bus: EventBus
    ):
        self.order_repository = order_repository
        self.product_repository = product_repository
        self.event_bus = event_bus
    
    def handle_create_order(self, command: CreateOrderCommand) -> Order:
        """
        Traiter la commande CreateOrder
        
        1. Valider
        2. Créer l'entité
        3. Persister
        4. Publier l'événement
        """
        
        # 1. Valider et récupérer les produits
        order_items = []
        
        for item_data in command.items:
            product = self.product_repository.find_by_id(item_data['product_id'])
            
            if not product:
                raise ValueError(f"Product {item_data['product_id']} not found")
            
            if product.stock < item_data['quantity']:
                raise ValueError(f"Insufficient stock for {product.name}")
            
            order_item = OrderItem(
                product_id=product.id,
                product_name=product.name,
                price=product.price,
                quantity=item_data['quantity']
            )
            order_items.append(order_item)
        
        # 2. Créer l'entité Order
        order = Order(
            user_id=command.user_id,
            items=order_items
        )
        
        order.validate()
        
        # 3. Réduire les stocks
        for item_data in command.items:
            product = self.product_repository.find_by_id(item_data['product_id'])
            product.reduce_stock(item_data['quantity'])
            self.product_repository.update(product)
        
        # 4. Persister
        saved_order = self.order_repository.save(order)
        
        # 5. Publier l'événement OrderPlaced
        event = OrderPlacedEvent.create(
            order_id=saved_order.id,
            user_id=saved_order.user_id,
            items=[
                {
                    'product_id': item.product_id,
                    'product_name': item.product_name,
                    'price': item.price,
                    'quantity': item.quantity
                }
                for item in saved_order.items
            ],
            total=saved_order.calculate_total()
        )
        
        self.event_bus.publish(event)
        
        print(f"[OK] Order {saved_order.id} created and event published")
        
        return saved_order
```

---

### [ENTREE] EVENT HANDLERS

**application/handlers/event_handlers/email_event_handler.py**

```python
from domain.events.base_event import BaseEvent
from domain.events.order_events import (
    OrderPlacedEvent,
    OrderShippedEvent,
    OrderCancelledEvent
)
from infrastructure.event_bus.event_bus_interface import EventBus

class EmailEventHandler:
    """
    Event Handler : Envoie des emails en réponse aux événements
    
    S'abonne à :
    - OrderPlaced -> Email de confirmation
    - OrderShipped -> Email avec tracking
    - OrderCancelled -> Email d'annulation
    """
    
    def __init__(self, event_bus: EventBus, email_service):
        self.event_bus = event_bus
        self.email_service = email_service
        
        # S'abonner aux événements
        self.event_bus.subscribe('OrderPlaced', self.on_order_placed)
        self.event_bus.subscribe('OrderShipped', self.on_order_shipped)
        self.event_bus.subscribe('OrderCancelled', self.on_order_cancelled)
        
        print("[EMAIL] EmailEventHandler initialized and subscribed")
    
    def on_order_placed(self, event: BaseEvent):
        """
        Réagir à OrderPlaced
        
        Envoyer un email de confirmation
        """
        print(f"[EMAIL] Handling OrderPlaced event: {event.event_id}")
        
        try:
            order_id = event.data['order_id']
            user_id = event.data['user_id']
            total = event.data['total']
            
            # Récupérer l'utilisateur
            user = self.email_service.get_user(user_id)
            
            # Envoyer l'email
            self.email_service.send_email(
                to=user.email,
                subject=f"Confirmation de commande #{order_id}",
                body=f"""
                Bonjour {user.name},
                
                Votre commande #{order_id} a été confirmée !
                
                Total : {total}€
                
                Vous recevrez un email de suivi dès que votre commande sera expédiée.
                
                Merci pour votre commande !
                """
            )
            
            print(f"[OK] Confirmation email sent for order {order_id}")
            
        except Exception as e:
            print(f"[X] Error sending confirmation email: {e}")
            # Log l'erreur mais ne fait pas échouer le handler
    
    def on_order_shipped(self, event: BaseEvent):
        """Réagir à OrderShipped"""
        print(f"[EMAIL] Handling OrderShipped event: {event.event_id}")
        
        try:
            order_id = event.data['order_id']
            tracking_number = event.data['tracking_number']
            carrier = event.data['carrier']
            
            # Récupérer la commande pour avoir le user_id
            order = self.email_service.get_order(order_id)
            user = self.email_service.get_user(order.user_id)
            
            # Envoyer l'email
            self.email_service.send_email(
                to=user.email,
                subject=f"Votre commande #{order_id} a été expédiée",
                body=f"""
                Bonjour {user.name},
                
                Votre commande #{order_id} a été expédiée !
                
                Transporteur : {carrier}
                Numéro de suivi : {tracking_number}
                
                Suivez votre colis : https://tracking.example.com/{tracking_number}
                """
            )
            
            print(f"[OK] Shipping email sent for order {order_id}")
            
        except Exception as e:
            print(f"[X] Error sending shipping email: {e}")
    
    def on_order_cancelled(self, event: BaseEvent):
        """Réagir à OrderCancelled"""
        print(f"[EMAIL] Handling OrderCancelled event: {event.event_id}")
        
        try:
            order_id = event.data['order_id']
            user_id = event.data['user_id']
            reason = event.data['reason']
            
            user = self.email_service.get_user(user_id)
            
            self.email_service.send_email(
                to=user.email,
                subject=f"Commande #{order_id} annulée",
                body=f"""
                Bonjour {user.name},
                
                Votre commande #{order_id} a été annulée.
                
                Raison : {reason}
                
                Vous serez remboursé sous 3-5 jours ouvrés.
                """
            )
            
            print(f"[OK] Cancellation email sent for order {order_id}")
            
        except Exception as e:
            print(f"[X] Error sending cancellation email: {e}")
```

---

**application/handlers/event_handlers/inventory_event_handler.py**

```python
from domain.events.base_event import BaseEvent
from infrastructure.event_bus.event_bus_interface import EventBus
from infrastructure.repositories.product_repository import ProductRepository

class InventoryEventHandler:
    """
    Event Handler : Gère l'inventaire en réponse aux événements
    
    S'abonne à :
    - OrderPlaced -> Réduire le stock (déjà fait dans OrderCommandHandler)
    - OrderCancelled -> Restaurer le stock
    """
    
    def __init__(self, event_bus: EventBus, product_repository: ProductRepository):
        self.event_bus = event_bus
        self.product_repository = product_repository
        
        # S'abonner aux événements
        self.event_bus.subscribe('OrderCancelled', self.on_order_cancelled)
        
        print("[PACKAGE] InventoryEventHandler initialized and subscribed")
    
    def on_order_cancelled(self, event: BaseEvent):
        """
        Réagir à OrderCancelled
        
        Restaurer le stock des produits
        """
        print(f"[PACKAGE] Handling OrderCancelled event: {event.event_id}")
        
        try:
            order_id = event.data['order_id']
            
            # Récupérer la commande
            order = self.product_repository.get_order(order_id)
            
            # Restaurer le stock
            for item in order.items:
                product = self.product_repository.find_by_id(item.product_id)
                product.increase_stock(item.quantity)
                self.product_repository.update(product)
                
                print(f"[OK] Stock restored for product {product.name}: +{item.quantity}")
            
            print(f"[OK] Stock restored for cancelled order {order_id}")
            
        except Exception as e:
            print(f"[X] Error restoring stock: {e}")
```

---

### [WEB] API CONTROLLER

**api/rest/controllers/order_controller.py**

```python
from flask import Blueprint, request, jsonify
from application.handlers.command_handlers.order_command_handler import (
    OrderCommandHandler,
    CreateOrderCommand
)

order_bp = Blueprint('orders', __name__, url_prefix='/api/orders')

class OrderController:
    """
    Controller REST pour les commandes
    
    Responsabilités :
    - Recevoir les requêtes HTTP
    - Valider les entrées
    - Déléguer aux Command Handlers
    - Retourner les réponses HTTP
    """
    
    def __init__(self, order_command_handler: OrderCommandHandler):
        self.order_command_handler = order_command_handler
    
    def create_order(self):
        """
        POST /api/orders
        
        Créer une commande
        """
        try:
            # Extraire les données
            data = request.get_json()
            
            # Valider
            if 'user_id' not in data:
                return jsonify({'error': 'user_id is required'}), 400
            
            if 'items' not in data or not data['items']:
                return jsonify({'error': 'items are required'}), 400
            
            # Créer la commande
            command = CreateOrderCommand(
                user_id=data['user_id'],
                items=data['items']
            )
            
            # Déléguer au handler
            order = self.order_command_handler.handle_create_order(command)
            
            # Réponse
            return jsonify({
                'order_id': order.id,
                'status': 'success',
                'message': 'Order created successfully. You will receive a confirmation email shortly.'
            }), 201
            
        except ValueError as e:
            return jsonify({'error': str(e)}), 400
        
        except Exception as e:
            print(f"Error: {e}")
            return jsonify({'error': 'Internal server error'}), 500

# Enregistrement des routes
def register_routes(app, order_command_handler):
    controller = OrderController(order_command_handler)
    
    @order_bp.route('/', methods=['POST'])
    def create_order():
        return controller.create_order()
    
    app.register_blueprint(order_bp)
```

---

### [CONFIG] CONFIGURATION

**app.py (Point d'entrée)**

```python
from flask import Flask
from infrastructure.event_bus.rabbitmq_event_bus import RabbitMQEventBus
from infrastructure.event_bus.in_memory_event_bus import InMemoryEventBus
from application.handlers.command_handlers.order_command_handler import OrderCommandHandler
from application.handlers.event_handlers.email_event_handler import EmailEventHandler
from application.handlers.event_handlers.inventory_event_handler import InventoryEventHandler
from api.rest.controllers.order_controller import register_routes

def create_app(use_rabbitmq=False):
    """
    Factory pour créer l'application
    
    Args:
        use_rabbitmq: Si True, utilise RabbitMQ. Sinon, In-Memory.
    """
    
    app = Flask(__name__)
    
    # Event Bus
    if use_rabbitmq:
        event_bus = RabbitMQEventBus(host='localhost', port=5672)
        print("[TRANSPORT] Using RabbitMQ Event Bus")
    else:
        event_bus = InMemoryEventBus()
        print("[TRANSPORT] Using In-Memory Event Bus (for development)")
    
    # Repositories (à configurer selon votre DB)
    order_repository = ...  # OrderRepository
    product_repository = ...  # ProductRepository
    
    # Command Handler
    order_command_handler = OrderCommandHandler(
        order_repository=order_repository,
        product_repository=product_repository,
        event_bus=event_bus
    )
    
    # Event Handlers (subscribers)
    email_handler = EmailEventHandler(event_bus, email_service)
    inventory_handler = InventoryEventHandler(event_bus, product_repository)
    
    # API Routes
    register_routes(app, order_command_handler)
    
    return app

if __name__ == '__main__':
    app = create_app(use_rabbitmq=True)
    app.run(host='0.0.0.0', port=5000, debug=True)
```

---

## [TEST] TESTS EVENT-DRIVEN

### Tests avec In-Memory Event Bus

```python
# tests/unit/test_order_event_flow.py
import pytest
from infrastructure.event_bus.in_memory_event_bus import InMemoryEventBus
from application.handlers.command_handlers.order_command_handler import (
    OrderCommandHandler,
    CreateOrderCommand
)
from application.handlers.event_handlers.email_event_handler import EmailEventHandler

def test_order_placed_triggers_email():
    """
    Test : Créer une commande publie OrderPlaced
           EmailHandler envoie un email
    """
    
    # Arrange
    event_bus = InMemoryEventBus()
    
    # Mock repositories
    mock_order_repo = Mock()
    mock_product_repo = Mock()
    mock_email_service = Mock()
    
    # Configurer les mocks
    mock_product = Product(id=1, name='Product 1', price=50, stock=100)
    mock_product_repo.find_by_id.return_value = mock_product
    
    mock_saved_order = Order(order_id=1, user_id=1, items=[...])
    mock_order_repo.save.return_value = mock_saved_order
    
    # Command Handler
    command_handler = OrderCommandHandler(
        order_repository=mock_order_repo,
        product_repository=mock_product_repo,
        event_bus=event_bus
    )
    
    # Event Handler (subscriber)
    email_handler = EmailEventHandler(event_bus, mock_email_service)
    
    # Act
    command = CreateOrderCommand(
        user_id=1,
        items=[{'product_id': 1, 'quantity': 2}]
    )
    
    order = command_handler.handle_create_order(command)
    
    # Assert
    # Vérifier que l'événement a été publié
    assert len(event_bus.published_events) == 1
    assert event_bus.published_events[0].event_type == 'OrderPlaced'
    
    # Vérifier que l'email a été envoyé
    mock_email_service.send_email.assert_called_once()
    
    # [OK] Test du flux complet en mémoire
    # [OK] Rapide (pas de RabbitMQ)
    # [OK] Déterministe

def test_multiple_handlers_receive_same_event():
    """
    Test : Un événement est reçu par TOUS les abonnés
    """
    
    # Arrange
    event_bus = InMemoryEventBus()
    
    handler1_called = False
    handler2_called = False
    handler3_called = False
    
    def handler1(event):
        nonlocal handler1_called
        handler1_called = True
    
    def handler2(event):
        nonlocal handler2_called
        handler2_called = True
    
    def handler3(event):
        nonlocal handler3_called
        handler3_called = True
    
    # S'abonner
    event_bus.subscribe('OrderPlaced', handler1)
    event_bus.subscribe('OrderPlaced', handler2)
    event_bus.subscribe('OrderPlaced', handler3)
    
    # Act
    event = OrderPlacedEvent.create(
        order_id=1,
        user_id=1,
        items=[],
        total=100
    )
    
    event_bus.publish(event)
    
    # Assert
    assert handler1_called
    assert handler2_called
    assert handler3_called
    
    # [OK] Tous les handlers ont été appelés
```

---

## [OBJECTIF] QUAND UTILISER EVENT-DRIVEN ?

### [OK] Utilise Event-Driven SI :

```
1. Besoin de DÉCOUPLAGE fort
   - Plusieurs services indépendants
   - Évolution indépendante des services
   - Équipes autonomes

2. Besoin de SCALABILITÉ différenciée
   - Certaines parties ont beaucoup plus de charge
   - Traitement asynchrone possible

3. Besoin d'EXTENSIBILITÉ
   - Ajouter des fonctionnalités sans modifier l'existant
   - Plugins / Extensions

4. Besoin d'AUDIT / HISTORIQUE
   - Event Sourcing
   - Traçabilité complète
   - Compliance

5. Architecture MICROSERVICES
   - Communication inter-services
   - Résilience
   - Eventual consistency acceptable

6. Workflows COMPLEXES
   - Saga pattern
   - Compensation
   - Long-running processes
```

---

### [X] N'utilise PAS Event-Driven SI :

```
1. Application SIMPLE
   - CRUD basique
   - Pas de logique complexe
   - Pas de workflows

2. Besoin de COHÉRENCE IMMÉDIATE
   - Transactions ACID obligatoires
   - Eventual consistency inacceptable
   - Réponse synchrone requise

3. Équipe PETITE ou PEU EXPÉRIMENTÉE
   - Courbe d'apprentissage raide
   - Debugging difficile
   - Opérations complexes

4. Infrastructure LIMITÉE
   - Pas de budget pour RabbitMQ/Kafka
   - Pas d'expertise DevOps
   - Environnement simple

5. Projet COURT TERME
   - Prototype
   - MVP rapide
   - POC

COMMENCE SIMPLE,
ajoute Event-Driven SI NÉCESSAIRE
```

---

## [BRAVO] CONCLUSION

### Ce que tu as appris

**Architecture Event-Driven :**
- [OK] Communication par événements (pub/sub)
- [OK] Découplage total des composants
- [OK] Event Bus (RabbitMQ, Kafka, In-Memory)
- [OK] Publishers et Subscribers
- [OK] Patterns (Pub/Sub, Event Streaming, Event Sourcing, CQRS)
- [OK] Avantages (scalabilité, extensibilité, résilience)
- [OK] Inconvénients (complexité, eventual consistency, debugging)

**Pratique :**
- [OK] Exemple e-commerce complet
- [OK] Événements immutables
- [OK] Event Bus RabbitMQ et In-Memory
- [OK] Command Handlers et Event Handlers
- [OK] Tests du flux événementiel

---

### Points clés

```
[OBJECTIF] Event-Driven = Communication par événements
[OBJECTIF] Publisher publie, ne connaît pas les subscribers
[OBJECTIF] Subscribers s'abonnent, ne connaissent pas le publisher
[OBJECTIF] Découplage total, scalabilité, extensibilité
[OBJECTIF] Eventual consistency (pas immédiat)
[OBJECTIF] Idéal pour microservices et workflows complexes
[OBJECTIF] Complexité opérationnelle élevée
```

---

### Évolution progressive

```
PHASE 1 : Synchrone (appels directs)
   -> Simple, facile

PHASE 2 : Asynchrone (queues simples)
   -> Background jobs
   -> RabbitMQ basique

PHASE 3 : Event-Driven (pub/sub)
   -> Événements
   -> Subscribers multiples

PHASE 4 : Event Sourcing
   -> Persistance des événements
   -> Rebuild d'état

PHASE 5 : CQRS + Event Sourcing
   -> Séparation Read/Write
   -> Scalabilité maximale
```

---

### Prochaines étapes

1. Code l'exemple e-commerce Event-Driven
2. Compare avec architecture synchrone
3. Expérimente avec RabbitMQ
4. Lis sur Event Sourcing et CQRS

**[RAPIDE] Tu maîtrises maintenant l'Architecture Event-Driven ! [RAPIDE]**

═══════════════════════════════════════════════════════════════
FIN DU FICHIER : architecture_event_driven.txt
═══════════════════════════════════════════════════════════════


# [OBJECTIF] ARCHITECTURES LOGICIELLES - GUIDE COMPARATIF COMPLET

## * INTRODUCTION

Ce guide est une **synthèse complète** des 8 architectures logicielles majeures. Il t'aide à :
- [OK] Comprendre les différences entre les architectures
- [OK] Choisir la bonne architecture pour ton projet
- [OK] Évoluer progressivement d'une architecture à l'autre
- [OK] Éviter les erreurs communes

**Les 8 architectures couvertes :**
1. Monolithique
2. MVC (Modèle-Vue-Contrôleur)
3. Layered (En couches / N-tier)
4. Clean Architecture
5. Hexagonale (Ports & Adapters)
6. Microservices
7. Event-Driven (Pilotée par événements)
8. Combinaisons (Hybrid)

---

## [GRAPHIQUE] TABLEAU COMPARATIF GLOBAL

### Vue d'ensemble

| Architecture | Complexité | Couplage | Testabilité | Scalabilité | Maintenance | Coût Initial | Équipe | Projet Type |
|--------------|------------|----------|-------------|-------------|-------------|--------------|--------|-------------|
| **Monolithique** | * | [X] Fort | ** | ** | ** | [ARGENT] | 1-5 | < 6 mois |
| **MVC** | ** | [ATTENTION] Moyen | *** | ** | *** | [ARGENT] | 2-8 | 6-12 mois |
| **Layered** | *** | [OK] Faible | *** | *** | **** | [ARGENT][ARGENT] | 3-10 | 12-24 mois |
| **Clean** | **** | [OK][OK] Très faible | ***** | **** | ***** | [ARGENT][ARGENT][ARGENT] | 3-10 | > 12 mois |
| **Hexagonale** | **** | [OK][OK] Très faible | ***** | **** | ***** | [ARGENT][ARGENT][ARGENT] | 3-10 | > 12 mois |
| **Microservices** | ***** | [OK][OK][OK] Minimal | **** | ***** | *** | [ARGENT][ARGENT][ARGENT][ARGENT][ARGENT] | 10+ | > 24 mois |
| **Event-Driven** | ***** | [OK][OK][OK] Minimal | **** | ***** | *** | [ARGENT][ARGENT][ARGENT][ARGENT] | 5-15 | > 18 mois |

---

### Détails par critère

**Complexité (Facilité de compréhension et mise en œuvre) :**

```
* Monolithique
   └─ Un fichier, code linéaire, pas de structure
   └─ Temps de compréhension : < 1 jour

** MVC
   └─ 3 couches (Modèle-Vue-Contrôleur)
   └─ Temps de compréhension : 1-3 jours

*** Layered
   └─ 4+ couches bien définies
   └─ Temps de compréhension : 3-7 jours

**** Clean/Hexagonale
   └─ Cercles/Hexagone, ports, adaptateurs
   └─ Temps de compréhension : 2-4 semaines

***** Microservices/Event-Driven
   └─ Multiple services, communication réseau/événements
   └─ Temps de compréhension : 1-3 mois
```

---

**Couplage (Dépendances entre composants) :**

```
[X] Fort (Monolithique)
   └─ Tout est interconnecté
   └─ Changer A affecte B, C, D...
   └─ Modification = risque élevé

[ATTENTION] Moyen (MVC)
   └─ Séparation M-V-C
   └─ Mais dépendances entre couches
   └─ Modification = risque moyen

[OK] Faible (Layered)
   └─ Couches indépendantes
   └─ Communication via interfaces
   └─ Modification = risque faible

[OK][OK] Très faible (Clean/Hexagonale)
   └─ Core complètement isolé
   └─ Adaptateurs interchangeables
   └─ Modification = risque minimal

[OK][OK][OK] Minimal (Microservices/Event-Driven)
   └─ Services totalement indépendants
   └─ Communication via API/événements
   └─ Modification = zéro impact sur autres services
```

---

## [OBJECTIF] GUIDE DE DÉCISION

### Arbre de décision

```
1. Taille du projet ?
   │
   ├─ Très petit (< 1K lignes, < 1 mois)
   │  └─-> MONOLITHIQUE
   │
   ├─ Petit (1K-10K lignes, 1-6 mois)
   │  └─-> MVC
   │
   ├─ Moyen (10K-50K lignes, 6-18 mois)
   │  └─-> 2. Besoin de tests exhaustifs ?
   │      ├─ Oui -> LAYERED ou CLEAN
   │      └─ Non -> MVC ou LAYERED
   │
   ├─ Grand (50K-100K lignes, 18-36 mois)
   │  └─-> 3. Besoin d'indépendance tech ?
   │      ├─ Oui -> CLEAN ou HEXAGONALE
   │      └─ Non -> LAYERED
   │
   └─ Très grand (> 100K lignes, > 36 mois)
      └─-> 4. Équipe > 10 développeurs ?
          ├─ Oui -> 5. Communication synchrone/asynchrone ?
          │         ├─ Synchrone -> MICROSERVICES
          │         └─ Asynchrone -> EVENT-DRIVEN + MICROSERVICES
          │
          └─ Non -> CLEAN ou HEXAGONALE
```

---

### Cas d'usage par architecture

**1. MONOLITHIQUE - Quand l'utiliser ?**

```
[OK] Cas d'usage parfaits :
├─ MVP / Prototype / POC
├─ Startup en validation d'idée
├─ Projet interne simple
├─ Application jetable
├─ Time-to-market < 1 mois
└─ Budget très limité

Exemples concrets :
• Landing page avec formulaire
• Outil interne d'administration
• Script automation
• Bot Discord/Slack simple
• Portfolio personnel

Entreprises qui utilisent (au début) :
• Instagram (début) - Python monolithe
• Twitter (début) - Ruby on Rails monolithe
• Facebook (début) - PHP monolithe
```

---

**2. MVC - Quand l'utiliser ?**

```
[OK] Cas d'usage parfaits :
├─ Application web classique
├─ CRUD avec interface utilisateur
├─ Blog, CMS, Forum
├─ Dashboard administratif
├─ Application avec UI importante
└─ Projet nécessitant séparation UI/Logique

Exemples concrets :
• Blog avec articles/commentaires
• E-commerce simple
• Plateforme de cours en ligne
• Système de gestion (CRM, ERP basique)
• Application SaaS MVP

Frameworks qui l'utilisent :
• Django (Python) - MTV (variant de MVC)
• Ruby on Rails
• Laravel (PHP)
• ASP.NET MVC
• Spring MVC (Java)
```

---

**3. LAYERED (En couches) - Quand l'utiliser ?**

```
[OK] Cas d'usage parfaits :
├─ Application d'entreprise moyenne
├─ Système de gestion important
├─ Application bancaire/finance
├─ Logiciel médical
├─ Application nécessitant conformité
└─ Projet avec équipe 3-10 dev

Exemples concrets :
• Système bancaire
• Plateforme e-commerce moyenne
• Application de gestion hospitalière
• ERP/CRM d'entreprise
• Application de réservation

Industries qui l'utilisent :
• Banque et finance
• Healthcare
• Gouvernement
• Assurance
• Entreprises traditionnelles
```

---

**4. CLEAN ARCHITECTURE - Quand l'utiliser ?**

```
[OK] Cas d'usage parfaits :
├─ Projet critique long terme (> 2 ans)
├─ Besoin de changer de technologie
├─ Tests exhaustifs obligatoires
├─ Logiciel vendu (produit)
├─ Application mission-critique
└─ Multiples interfaces (Web + Mobile + CLI + API)

Exemples concrets :
• Plateforme de trading financier
• Système de gestion hospitalier critique
• Application aérospatiale
• Logiciel de sécurité
• Infrastructure bancaire core

Entreprises qui l'utilisent :
• Banks (systèmes core)
• Healthcare (dossiers patients)
• Defense contractors
• Financial institutions
• Entreprises avec legacy migration
```

---

**5. HEXAGONALE (Ports & Adapters) - Quand l'utiliser ?**

```
[OK] Cas d'usage parfaits :
├─ Même que Clean Architecture
├─ Besoin de multiples adaptateurs
├─ Migration progressive
├─ Tests avec multiples implémentations
├─ Domain-Driven Design (DDD)
└─ Besoin de brancher différentes techs

Exemples concrets :
• Application multi-tenant
• Plateforme avec plusieurs DB (SQL, NoSQL, etc.)
• Application avec multiples paiements (Stripe, PayPal, etc.)
• Système avec plusieurs cloud providers
• Application qui change souvent de techno

Similaire à Clean Architecture mais :
• Visualisation différente (hexagone vs cercles)
• Emphasis sur la symétrie
• Plus facile à expliquer avec les "ports"
```

---

**6. MICROSERVICES - Quand l'utiliser ?**

```
[OK] Cas d'usage parfaits :
├─ Très grande application (> 100K lignes)
├─ Équipe > 10 développeurs
├─ Besoin de scalabilité différenciée
├─ Déploiements indépendants critiques
├─ Équipes autonomes
└─ Budget confortable + expertise DevOps

Exemples concrets :
• Netflix (2,800+ microservices)
• Uber (2,200+ microservices)
• Amazon (milliers de microservices)
• Spotify (800+ microservices)
• eBay (220+ microservices)

Cas d'usage spécifiques :
• Streaming vidéo (Netflix)
• Marketplace (Amazon, eBay)
• Ride-sharing (Uber, Lyft)
• Social media à grande échelle
• SaaS avec millions d'utilisateurs
```

---

**7. EVENT-DRIVEN - Quand l'utiliser ?**

```
[OK] Cas d'usage parfaits :
├─ Workflows complexes
├─ Communication asynchrone nécessaire
├─ Besoin d'audit trail complet
├─ IoT (Internet of Things)
├─ Real-time analytics
└─ Besoin de découplage total

Exemples concrets :
• Plateforme e-commerce (OrderPlaced -> Email + Inventory + Analytics)
• IoT (capteurs -> événements -> traitement)
• Trading platform (tick data streaming)
• Social media (posts -> notifications -> analytics)
• Gaming backend (actions -> événements)

Entreprises qui l'utilisent :
• LinkedIn (Apache Kafka)
• Netflix (Event-driven microservices)
• Uber (Real-time location events)
• Twitter (Tweet events)
• Airbnb (Booking events)
```

---

## [SYNC] ÉVOLUTION PROGRESSIVE

### Scénario réel : Startup -> Entreprise

**ANNÉE 1 : MONOLITHIQUE**

```
Équipe : 2 développeurs
Utilisateurs : 100
Code : 5,000 lignes

Structure :
app.py (toute l'application)
templates/
static/
database.db

[OK] Rapide à développer
[OK] Facile à déployer (1 serveur)
[OK] Coût : 50€/mois
```

---

**ANNÉE 2 : MVC**

```
Équipe : 4 développeurs
Utilisateurs : 5,000
Code : 25,000 lignes

Migration :
1. Séparer en Modèles
2. Créer des Vues
3. Extraire les Contrôleurs

Structure :
models/
  user.py
  product.py
  order.py
views/
  templates/
controllers/
  user_controller.py
  product_controller.py

[OK] Plus structuré
[OK] Tests possibles
[OK] Coût : 100€/mois
```

---

**ANNÉE 3 : LAYERED**

```
Équipe : 8 développeurs
Utilisateurs : 50,000
Code : 80,000 lignes

Migration :
1. Créer couche Services (logique métier)
2. Créer couche Repositories (data access)
3. Créer couche Présentation

Structure :
presentation/
  controllers/
  api/
  dto/
business/
  services/
  domain/
data_access/
  repositories/
  models/

[OK] Testabilité améliorée
[OK] Maintenance facilitée
[OK] Coût : 300€/mois
```

---

**ANNÉE 4 : CLEAN/HEXAGONALE**

```
Équipe : 10 développeurs
Utilisateurs : 200,000
Code : 150,000 lignes

Migration :
1. Extraire le Core (Use Cases + Entities)
2. Définir les Ports (interfaces)
3. Créer les Adaptateurs

Structure :
core/
  domain/
  use_cases/
  ports/
adapters/
  primary/
    web/
    cli/
  secondary/
    postgresql/
    redis/

[OK] Indépendance tech
[OK] Migration facile
[OK] Coût : 500€/mois
```

---

**ANNÉE 5+ : MICROSERVICES + EVENT-DRIVEN**

```
Équipe : 30+ développeurs (6 équipes)
Utilisateurs : 1,000,000+
Code : 300,000+ lignes (répartis)

Migration progressive :
1. Extraire service Auth (petit, isolé)
2. Extraire service Notifications
3. Extraire service Payment
4. Ajouter Event Bus (RabbitMQ/Kafka)
5. Communication asynchrone

Structure :
services/
  auth-service/
  user-service/
  product-service/
  order-service/
  payment-service/
  notification-service/
event-bus/
  kafka/
api-gateway/

[OK] Scalabilité maximale
[OK] Équipes autonomes
[OK] Coût : 5,000€/mois
```

---

## [IDEE] ERREURS COMMUNES À ÉVITER

### 1. Erreur : "Je dois utiliser Microservices dès le début"

```
[X] MAUVAIS :
Startup de 2 personnes -> Microservices
  ├─ 10 services à gérer
  ├─ Kubernetes, Docker, CI/CD
  ├─ Event Bus, Service Discovery
  └─ Résultat : 6 mois de setup, pas de produit

[OK] BON :
Startup de 2 personnes -> Monolithique
  ├─ 1 application simple
  ├─ Déploiement en 1 jour
  └─ Résultat : Produit en 2 semaines, itérations rapides

RÈGLE : Commence simple, évolue si nécessaire
```

---

### 2. Erreur : "Je reste en Monolithique alors que ça devient ingérable"

```
[X] MAUVAIS :
Application de 500,000 lignes en 1 fichier
  ├─ 20 développeurs qui se marchent dessus
  ├─ Déploiement = 2 heures
  ├─ Tests = impossible
  └─ Bug dans une partie = tout crash

[OK] BON :
Migrer progressivement vers Layered ou Clean
  ├─ Structurer par couches
  ├─ Isoler la logique métier
  ├─ Tests par couche
  └─ Déploiement plus sûr

RÈGLE : Si > 50K lignes ET > 5 dev -> Structure nécessaire
```

---

### 3. Erreur : "J'utilise Clean Architecture pour un CRUD simple"

```
[X] MAUVAIS :
Todo List simple -> Clean Architecture
  ├─ 50 fichiers pour 3 fonctions (Create, Read, Delete)
  ├─ 2 semaines de développement
  └─ Overkill total

[OK] BON :
Todo List simple -> MVC ou même Monolithique
  ├─ 5 fichiers
  ├─ 1 jour de développement
  └─ Suffisant pour le besoin

RÈGLE : Architecture proportionnelle à la complexité
```

---

### 4. Erreur : "Je passe directement de Monolithique à Microservices"

```
[X] MAUVAIS :
Monolithique (50K lignes) -> Microservices (10 services)
  └─ Migration big-bang
  └─ 6 mois de migration
  └─ Tout casse
  └─ Rollback impossible

[OK] BON :
Monolithique -> Layered -> Clean -> Microservices progressifs
  └─ Migration incrémentale
  └─ 1 service à la fois
  └─ Rollback possible
  └─ 12 mois de migration sécurisée

RÈGLE : Migration progressive, 1 service à la fois
```

---

### 5. Erreur : "J'utilise Event-Driven partout"

```
[X] MAUVAIS :
Tout en événements, même pour :
  ├─ "Récupérer un utilisateur par ID" -> Événement
  ├─ "Calculer 2+2" -> Événement
  └─ Complexité inutile

[OK] BON :
Event-Driven UNIQUEMENT pour :
  ├─ Actions qui déclenchent des workflows
  ├─ Communication asynchrone nécessaire
  ├─ Découplage requis
  └─ Exemple : OrderPlaced -> Email + Inventory + Analytics

RÈGLE : Synchrone par défaut, Asynchrone si justifié
```

---

## [COURS] COMBINAISONS D'ARCHITECTURES

Les architectures ne sont PAS mutuellement exclusives. On peut les combiner !

### Combinaison 1 : Clean + Event-Driven

```
Utilisation : Application critique avec workflows asynchrones

Structure :
core/
  domain/
  use_cases/
    - CreateOrderUseCase -> Publie OrderPlacedEvent
  ports/
events/
  - OrderPlacedEvent
  - PaymentCompletedEvent
adapters/
  primary/
    web/
  secondary/
    postgresql/
    rabbitmq/

Avantages :
[OK] Core isolé (Clean)
[OK] Communication découplée (Event-Driven)
[OK] Testabilité maximale
[OK] Workflows complexes gérables

Exemples :
• Fintech (trading + compliance + audit)
• Healthcare (dossiers + notifications + analytics)
```

---

### Combinaison 2 : Microservices + Clean (par service)

```
Utilisation : Chaque microservice est structuré en Clean Architecture

Structure globale :
services/
  user-service/     <- Clean Architecture
    core/
    adapters/
  order-service/    <- Clean Architecture
    core/
    adapters/
  payment-service/  <- Clean Architecture
    core/
    adapters/

Avantages :
[OK] Scalabilité (Microservices)
[OK] Testabilité de chaque service (Clean)
[OK] Indépendance tech par service
[OK] Migration facile d'un service

Exemples :
• Netflix (microservices structurés)
• Amazon (services indépendants, chacun bien structuré)
```

---

### Combinaison 3 : Layered + MVC (Web) + API (REST)

```
Utilisation : Application classique avec UI et API

Structure :
presentation/
  web/          <- MVC pour l'UI web
    models/
    views/
    controllers/
  api/          <- REST pour mobile/externe
    routes/
business/
  services/
data_access/
  repositories/

Avantages :
[OK] UI structurée (MVC)
[OK] API pour mobile
[OK] Logique partagée (Services)

Exemples :
• E-commerce avec site web + app mobile
• SaaS avec dashboard + API publique
```

---

### Combinaison 4 : Event-Driven + CQRS

```
Utilisation : Séparation lecture/écriture avec événements

Structure :
write-side/
  commands/
    - CreateOrderCommand
  handlers/
    - CreateOrderHandler -> Publie événement
  event-store/

read-side/
  projections/
    - OrderListProjection (écoute événements)
    - OrderDetailsProjection
  queries/
    - GetOrderQuery

Avantages :
[OK] Scalabilité read/write indépendante
[OK] Optimisation séparée
[OK] Audit trail complet (Event Store)

Exemples :
• Trading platform (write lent, read ultra rapide)
• Analytics dashboard (millions de lectures, peu d'écritures)
```

---

## [HAUSSE] MÉTRIQUES DE DÉCISION

### Tableau de scoring

Évalue ton projet sur ces critères (0-5) :

```
Critère                           Score   Poids   Total
─────────────────────────────────────────────────────
Complexité métier                 __/5    × 3     = __
Taille équipe (0=1, 5=20+)        __/5    × 2     = __
Durée projet (0=<1m, 5=>2ans)     __/5    × 2     = __
Besoin de tests (0=non, 5=oui)    __/5    × 3     = __
Besoin scalabilité (0=non, 5=oui) __/5    × 2     = __
Budget disponible (0=faible, 5=élevé) __/5 × 1    = __
Expertise équipe (0=junior, 5=senior) __/5 × 2    = __
                                          ─────────
                                  TOTAL PONDÉRÉ : __/70

Interprétation :
0-15   -> Monolithique
16-25  -> MVC
26-35  -> Layered
36-45  -> Clean/Hexagonale
46-55  -> Microservices
56-70  -> Microservices + Event-Driven
```

---

### Exemple d'évaluation

**Projet A : Blog personnel**

```
Complexité métier               1/5  × 3 = 3
Taille équipe                   0/5  × 2 = 0
Durée projet                    0/5  × 2 = 0
Besoin de tests                 1/5  × 3 = 3
Besoin scalabilité              1/5  × 2 = 2
Budget disponible               1/5  × 1 = 1
Expertise équipe                2/5  × 2 = 4
                                ──────────
                        TOTAL : 13/70

-> Recommandation : MONOLITHIQUE ou MVC simple
```

---

**Projet B : Plateforme e-commerce moyenne**

```
Complexité métier               3/5  × 3 = 9
Taille équipe                   2/5  × 2 = 4  (5 dev)
Durée projet                    3/5  × 2 = 6  (18 mois)
Besoin de tests                 4/5  × 3 = 12
Besoin scalabilité              3/5  × 2 = 6
Budget disponible               3/5  × 1 = 3
Expertise équipe                3/5  × 2 = 6
                                ──────────
                        TOTAL : 46/70

-> Recommandation : CLEAN ou début de MICROSERVICES
```

---

**Projet C : Netflix-like streaming**

```
Complexité métier               5/5  × 3 = 15
Taille équipe                   5/5  × 2 = 10  (50+ dev)
Durée projet                    5/5  × 2 = 10  (continu)
Besoin de tests                 5/5  × 3 = 15
Besoin scalabilité              5/5  × 2 = 10
Budget disponible               5/5  × 1 = 5
Expertise équipe                5/5  × 2 = 10
                                ──────────
                        TOTAL : 75/70 (max)

-> Recommandation : MICROSERVICES + EVENT-DRIVEN
```

---

## [OBJECTIF] CHECKLIST PAR ARCHITECTURE

### Checklist Monolithique

```
Utilise Monolithique si TOUTES ces conditions :
[WHITE_SQUARE] Équipe ≤ 3 développeurs
[WHITE_SQUARE] Projet < 6 mois
[WHITE_SQUARE] Code estimé < 10,000 lignes
[WHITE_SQUARE] Pas de besoin de scalabilité
[WHITE_SQUARE] Time-to-market critique
[WHITE_SQUARE] Pas de tests exhaustifs requis
[WHITE_SQUARE] Budget limité (< 100€/mois infrastructure)

Si 7/7 -> Monolithique
Si 5-6/7 -> Monolithique acceptable
Si < 5/7 -> Considérer MVC
```

---

### Checklist MVC

```
Utilise MVC si :
[WHITE_SQUARE] Application web avec UI importante
[WHITE_SQUARE] Équipe 2-8 développeurs
[WHITE_SQUARE] Projet 6-18 mois
[WHITE_SQUARE] Code estimé 10K-50K lignes
[WHITE_SQUARE] CRUD avec interface utilisateur
[WHITE_SQUARE] Tests souhaités mais pas critiques
[WHITE_SQUARE] Framework MVC disponible (Django, Rails, Laravel)

Si 5-7/7 -> MVC
Si < 5/7 -> Monolithique ou Layered
```

---

### Checklist Layered

```
Utilise Layered si :
[WHITE_SQUARE] Application d'entreprise
[WHITE_SQUARE] Équipe 3-10 développeurs
[WHITE_SQUARE] Projet 12-24 mois
[WHITE_SQUARE] Code estimé 50K-100K lignes
[WHITE_SQUARE] Logique métier importante
[WHITE_SQUARE] Tests obligatoires
[WHITE_SQUARE] Maintenance long terme
[WHITE_SQUARE] Standards d'entreprise

Si 6-8/8 -> Layered
Si < 6/8 -> MVC ou Clean
```

---

### Checklist Clean/Hexagonale

```
Utilise Clean/Hexagonale si :
[WHITE_SQUARE] Projet critique long terme (> 2 ans)
[WHITE_SQUARE] Équipe 3-10 développeurs expérimentés
[WHITE_SQUARE] Code estimé 50K-200K lignes
[WHITE_SQUARE] Besoin de changer de technologie
[WHITE_SQUARE] Tests exhaustifs OBLIGATOIRES
[WHITE_SQUARE] Multiple interfaces (Web + Mobile + CLI + API)
[WHITE_SQUARE] Domain-Driven Design
[WHITE_SQUARE] Budget pour complexité initiale

Si 6-8/8 -> Clean/Hexagonale
Si < 6/8 -> Layered ou Microservices
```

---

### Checklist Microservices

```
Utilise Microservices si :
[WHITE_SQUARE] Équipe > 10 développeurs
[WHITE_SQUARE] Projet > 24 mois
[WHITE_SQUARE] Code total > 100K lignes
[WHITE_SQUARE] Besoin scalabilité différenciée
[WHITE_SQUARE] Déploiements indépendants critiques
[WHITE_SQUARE] Équipes autonomes souhaitées
[WHITE_SQUARE] Expertise DevOps disponible
[WHITE_SQUARE] Budget confortable (> 1000€/mois infra)

Si 7-8/8 -> Microservices
Si 5-6/8 -> Clean d'abord, puis Microservices
Si < 5/8 -> Clean/Hexagonale
```

---

### Checklist Event-Driven

```
Utilise Event-Driven si :
[WHITE_SQUARE] Workflows complexes asynchrones
[WHITE_SQUARE] Besoin de découplage total
[WHITE_SQUARE] Communication asynchrone requise
[WHITE_SQUARE] Besoin d'audit trail complet
[WHITE_SQUARE] IoT ou real-time data
[WHITE_SQUARE] Event Sourcing souhaité
[WHITE_SQUARE] Expertise en messaging (RabbitMQ/Kafka)
[WHITE_SQUARE] Eventual consistency acceptable

Si 6-8/8 -> Event-Driven
Si 4-5/8 -> Hybride (Event-Driven partiel)
Si < 4/8 -> Synchrone classique
```

---

## [RAPIDE] PLAN D'ACTION

### Étape 1 : Évaluer ton projet actuel

```
Questions à te poser :

1. Quelle est la taille actuelle ?
   [WHITE_SQUARE] < 5K lignes
   [WHITE_SQUARE] 5K-20K lignes
   [WHITE_SQUARE] 20K-50K lignes
   [WHITE_SQUARE] 50K-100K lignes
   [WHITE_SQUARE] > 100K lignes

2. Combien de développeurs ?
   [WHITE_SQUARE] 1-2
   [WHITE_SQUARE] 3-5
   [WHITE_SQUARE] 6-10
   [WHITE_SQUARE] 11-20
   [WHITE_SQUARE] 20+

3. Quels sont les problèmes actuels ?
   [WHITE_SQUARE] Déploiements longs/risqués
   [WHITE_SQUARE] Tests difficiles
   [WHITE_SQUARE] Modifications impactent tout
   [WHITE_SQUARE] Scalabilité limitée
   [WHITE_SQUARE] Équipe bloquée
   [WHITE_SQUARE] Couplage fort
   [WHITE_SQUARE] Code spaghetti

4. Quel est le budget disponible ?
   [WHITE_SQUARE] < 100€/mois
   [WHITE_SQUARE] 100-500€/mois
   [WHITE_SQUARE] 500-2000€/mois
   [WHITE_SQUARE] 2000-5000€/mois
   [WHITE_SQUARE] > 5000€/mois
```

---

### Étape 2 : Choisir l'architecture cible

Utilise l'arbre de décision (page 2) et les checklists (pages précédentes).

```
Mon architecture actuelle : ___________________

Mon architecture cible : ___________________

Pourquoi ce choix ?
1. ___________________________________
2. ___________________________________
3. ___________________________________
```

---

### Étape 3 : Plan de migration

**Si migration nécessaire :**

```
MIGRATION PROGRESSIVE (recommandé)

Phase 1 : Préparation (1-2 mois)
[WHITE_SQUARE] Documenter l'architecture actuelle
[WHITE_SQUARE] Identifier les modules/boundaries
[WHITE_SQUARE] Écrire des tests (si absents)
[WHITE_SQUARE] Geler les nouvelles features

Phase 2 : Restructuration (2-4 mois)
[WHITE_SQUARE] Créer la nouvelle structure (dossiers)
[WHITE_SQUARE] Migrer module par module
[WHITE_SQUARE] Garder l'ancien code en parallèle
[WHITE_SQUARE] Tests à chaque étape

Phase 3 : Bascule (1 mois)
[WHITE_SQUARE] Rediriger le trafic progressivement
[WHITE_SQUARE] Monitoring intensif
[WHITE_SQUARE] Rollback plan prêt
[WHITE_SQUARE] Support 24/7

Phase 4 : Cleanup (1 mois)
[WHITE_SQUARE] Supprimer l'ancien code
[WHITE_SQUARE] Documentation finale
[WHITE_SQUARE] Formation équipe
[WHITE_SQUARE] Retour d'expérience

Total : 5-8 mois pour migration complète
```

**Exemple concret :**

```
Migration Monolithique -> Microservices :

Mois 1-2 : Tests et boundaries
Mois 3 : Extraire Auth Service (petit, isolé)
Mois 4 : Extraire Notification Service
Mois 5 : Extraire Payment Service
Mois 6 : Extraire Product Service
Mois 7 : Extraire Order Service (complexe)
Mois 8 : Cleanup + documentation

Services extraits : 5
Monolithe restant : Core business logic
```

---

### Étape 4 : Mesurer le succès

**Métriques à suivre :**

```
AVANT migration :
├─ Temps de déploiement : ___ minutes
├─ Fréquence de déploiement : ___ fois/mois
├─ Taux d'échec déploiement : ___ %
├─ Temps de build : ___ minutes
├─ Couverture de tests : ___ %
├─ Temps de résolution bugs : ___ jours
├─ Onboarding nouveau dev : ___ semaines
└─ Satisfaction équipe : ___ /10

APRÈS migration (objectifs) :
├─ Temps de déploiement : ___ minutes (v 50%)
├─ Fréquence de déploiement : ___ fois/mois (^ 200%)
├─ Taux d'échec déploiement : ___ % (v 70%)
├─ Temps de build : ___ minutes (v 60%)
├─ Couverture de tests : ___ % (^ 50%)
├─ Temps de résolution bugs : ___ jours (v 40%)
├─ Onboarding nouveau dev : ___ semaines (v 30%)
└─ Satisfaction équipe : ___ /10 (^ 2 points)
```

---

## [DOCS] RESSOURCES SUPPLÉMENTAIRES

### Livres recommandés

```
[GUIDE] Fondamentaux :
• Clean Code (Robert C. Martin)
• The Pragmatic Programmer (Hunt & Thomas)
• Design Patterns (Gang of Four)

[GUIDE] Architecture :
• Clean Architecture (Robert C. Martin)
• Building Microservices (Sam Newman)
• Domain-Driven Design (Eric Evans)
• Implementing Domain-Driven Design (Vaughn Vernon)
• Software Architecture Patterns (Mark Richards)

[GUIDE] Event-Driven :
• Designing Event-Driven Systems (Ben Stopford)
• Enterprise Integration Patterns (Gregor Hohpe)

[GUIDE] Microservices :
• Microservices Patterns (Chris Richardson)
• Production-Ready Microservices (Susan Fowler)
```

---

### Sites et documentation

```
[WEB] Documentation officielle :
• docs.claude.ai (Anthropic)
• docs.microsoft.com/azure/architecture
• aws.amazon.com/architecture
• martinfowler.com (Architecture patterns)

[COURS] Cours en ligne :
• Coursera - Software Architecture
• Udemy - Clean Architecture
• Pluralsight - Microservices Architecture

[STUDIO_MICROPHONE] Podcasts :
• Software Engineering Radio
• The Changelog
• Engineering Culture by InfoQ
```

---

### Communautés

```
[SPEECH_BALLOON] Forums :
• Stack Overflow (architecture tag)
• Reddit - r/softwarearchitecture
• Dev.to (architecture articles)

[UTILISATEURS] Meetups/Conférences :
• DDD Europe
• Microservices Practitioners Summit
• Software Architecture Conference (O'Reilly)
• QCon

[BIRD] Twitter :
• @martinfowler
• @samNewman (Microservices)
• @ericevans0 (DDD)
• @unclebobmartin (Clean Architecture)
```

---

## [COURS] EXERCICES PRATIQUES

### Exercice 1 : Analyse d'architecture *

```
Choisis un projet open source sur GitHub et :

1. Identifie l'architecture utilisée
2. Justifie pourquoi cette architecture
3. Propose une architecture alternative
4. Liste les avantages/inconvénients du changement

Projets suggérés :
• Django (GitHub: django/django)
• Flask (GitHub: pallets/flask)
• Ruby on Rails (GitHub: rails/rails)
• Express.js (GitHub: expressjs/express)
```

---

### Exercice 2 : Migration progressive **

```
Prends un de tes projets monolithiques et :

1. Documente l'architecture actuelle
2. Identifie les boundaries (modules logiques)
3. Choisis 1 module à extraire
4. Crée une branche de migration
5. Extrais ce module en service séparé
6. Garde les 2 versions en parallèle
7. Compare les résultats

Temps estimé : 1 semaine
```

---

### Exercice 3 : Implémentation multi-architecture ***

```
Implémente le MÊME projet (ex: Todo List) dans :

1. Monolithique (1 fichier)
2. MVC (Modèle-Vue-Contrôleur)
3. Layered (4 couches)
4. Clean Architecture

Compare :
• Nombre de fichiers
• Lignes de code
• Temps de développement
• Facilité de tests
• Facilité de modifications

Temps estimé : 2-3 semaines
```

---

### Exercice 4 : Event-Driven workflow ****

```
Crée un système de commande e-commerce avec :

1. OrderPlaced -> Événement
2. 3 subscribers :
   • EmailService (envoie confirmation)
   • InventoryService (réduit stock)
   • AnalyticsService (track vente)

Utilise :
• RabbitMQ ou In-Memory Event Bus
• Tests pour vérifier que tous les subscribers reçoivent
• Gestion des erreurs (retry, dead letter queue)

Temps estimé : 1 semaine
```

---

## [BRAVO] CONCLUSION

### Récapitulatif

**Tu as appris :**

[OK] **8 architectures** complètes avec code  
[OK] **Quand utiliser** chaque architecture  
[OK] **Comment migrer** progressivement  
[OK] **Erreurs à éviter**  
[OK] **Combinaisons** d'architectures  
[OK] **Métriques de décision**  
[OK] **Checklists** par architecture  
[OK] **Plan d'action** concret  

---

### Principes à retenir

```
1. COMMENCE SIMPLE
   └─ Monolithique -> MVC -> Layered -> Clean -> Microservices
   └─ N'anticipe pas la complexité

2. ARCHITECTURE = COMPROMIS
   └─ Pas d'architecture parfaite
   └─ Tout dépend du contexte
   └─ Chaque choix a des trade-offs

3. MIGRATION PROGRESSIVE
   └─ Big-bang = risque élevé
   └─ Incrémental = succès probable
   └─ 1 service/module à la fois

4. MESURE ET ADAPTE
   └─ Métriques objectives
   └─ Retour d'expérience
   └─ Amélioration continue

5. ÉQUIPE AVANT TECHNO
   └─ Architecture = compétences équipe
   └─ Formation nécessaire
   └─ Consensus important
```

---

### Prochaines étapes

```
[OBJECTIF] Court terme (1 mois) :
[WHITE_SQUARE] Évaluer ton projet actuel
[WHITE_SQUARE] Choisir architecture cible
[WHITE_SQUARE] Lire 1 livre d'architecture
[WHITE_SQUARE] Faire exercice 1 et 2

[OBJECTIF] Moyen terme (3 mois) :
[WHITE_SQUARE] Commencer migration (si nécessaire)
[WHITE_SQUARE] Implémenter exercice 3
[WHITE_SQUARE] Participer à 1 meetup
[WHITE_SQUARE] Contribuer à un projet open source

[OBJECTIF] Long terme (6+ mois) :
[WHITE_SQUARE] Maîtriser 3+ architectures
[WHITE_SQUARE] Faire exercice 4
[WHITE_SQUARE] Passer certification
[WHITE_SQUARE] Devenir référent architecture dans ton équipe
```

---

### Message final

```
L'ARCHITECTURE PARFAITE N'EXISTE PAS

[OK] Ce qui existe :
   └─ L'architecture ADAPTÉE à ton contexte
   └─ L'architecture que ton ÉQUIPE maîtrise
   └─ L'architecture qui répond aux BESOINS
   └─ L'architecture qui ÉVOLUE avec le projet

[X] Ce qui n'existe pas :
   └─ L'architecture universelle
   └─ L'architecture sans compromis
   └─ L'architecture figée

[OBJECTIF] TON OBJECTIF :
   └─ Choisir intelligemment
   └─ Migrer progressivement
   └─ Mesurer objectivement
   └─ S'adapter continuellement

[RAPIDE] TU AS MAINTENANT TOUS LES OUTILS POUR RÉUSSIR !
```

---

## [GRAPHIQUE] ANNEXE : TEMPLATE DE DÉCISION

Utilise ce template pour documenter tes choix d'architecture :

```markdown
# Architecture Decision Record (ADR)

Date : ___________
Projet : ___________
Équipe : ___________

## Contexte

Taille équipe : ___
Durée projet : ___
Budget : ___
Utilisateurs estimés : ___
Code existant : ___ lignes

## Problèmes actuels

1. ___________________________
2. ___________________________
3. ___________________________

## Options considérées

### Option 1 : ___________
Avantages :
- 
Inconvénients :
- 

### Option 2 : ___________
Avantages :
- 
Inconvénients :
- 

### Option 3 : ___________
Avantages :
- 
Inconvénients :
- 

## Décision

Architecture choisie : ___________

Raisons :
1. ___________________________
2. ___________________________
3. ___________________________

## Conséquences

Positives :
- 
Négatives :
- 
Risques :
- 

## Plan d'implémentation

Phase 1 (___) : _______________
Phase 2 (___) : _______________
Phase 3 (___) : _______________

## Métriques de succès

Avant : ___
Après (objectif) : ___
```

---

**[BRAVO] FÉLICITATIONS ! Tu as terminé le guide comparatif complet ! [BRAVO]**

**Tu es maintenant équipé pour faire les meilleurs choix architecturaux pour tes projets ! [RAPIDE]**

═══════════════════════════════════════════════════════════════
FIN DU FICHIER : architectures_comparaison_complete.txt
═══════════════════════════════════════════════════════════════