# Fichier: python_cheats/cheatsheets/design_pattern.txt
# Cheatsheet Design Patterns Python - Guide Complet pour Débutants
# Les 23 Patterns du Gang of Four (GoF)


╔════════════════════════════════════════════════════════════════════╗
║                    QU'EST-CE QU'UN DESIGN PATTERN?                 ║
╚════════════════════════════════════════════════════════════════════╝

Un design pattern est une SOLUTION RÉUTILISABLE à un problème courant 
en programmation. C'est comme une recette de cuisine pour résoudre 
un type de problème spécifique.

# Les 3 catégories:
# 1. CRÉATIONNELS (5) - Comment créer des objets
# 2. STRUCTURELS (7) - Comment organiser/composer les objets
# 3. COMPORTEMENTAUX (11) - Comment les objets interagissent


╔════════════════════════════════════════════════════════════════════╗
║                    PATTERNS CRÉATIONNELS (5)                       ║
╚════════════════════════════════════════════════════════════════════╝


[OK] 1. SINGLETON - "Un seul exemplaire dans tout le programme"

POURQUOI?
- Garantir qu'une classe n'a qu'UNE SEULE instance
- Exemple: gestionnaire de base de données, logger, configuration

QUAND?
- Besoin d'un point d'accès global unique
- Une seule instance doit contrôler une ressource (DB, fichier config)
- Éviter d'avoir plusieurs connexions DB inutiles

COMMENT?

# === Méthode 1: Classique avec __new__ ===

class Singleton:
    _instance = None
    
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance
    
    def __init__(self):
        self.value = "Je suis unique!"

# Utilisation
s1 = Singleton()
s2 = Singleton()
print(s1 is s2)  # True - Même instance!
s1.value = "Modifié"
print(s2.value)  # "Modifié" - Partagé!


# === Méthode 2: Avec décorateur ===

def singleton(cls):
    instances = {}
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return get_instance

@singleton
class DatabaseConnection:
    def __init__(self):
        self.connected = False
    
    def connect(self):
        if not self.connected:
            print("Connexion à la base de données...")
            self.connected = True
        else:
            print("Déjà connecté!")

# Utilisation
db1 = DatabaseConnection()
db2 = DatabaseConnection()
print(db1 is db2)  # True
db1.connect()      # Connexion à la base de données...
db2.connect()      # Déjà connecté!


# === Méthode 3: Thread-safe (pour applications multi-threads) ===

import threading

class ThreadSafeSingleton:
    _instance = None
    _lock = threading.Lock()
    
    def __new__(cls):
        if cls._instance is None:
            with cls._lock:
                # Double vérification
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
        return cls._instance


# === Exemple pratique: Logger ===

class Logger:
    _instance = None
    
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance.logs = []
        return cls._instance
    
    def log(self, message):
        self.logs.append(message)
        print(f"[LOG] {message}")
    
    def show_logs(self):
        return self.logs

# Utilisation dans différents modules
logger1 = Logger()
logger1.log("Démarrage de l'application")

logger2 = Logger()  # Même instance!
logger2.log("Utilisateur connecté")

print(logger1.show_logs())  # ['Démarrage...', 'Utilisateur...']
print(logger1 is logger2)   # True


# === Exemple pratique: Configuration ===

class AppConfig:
    _instance = None
    
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance.settings = {}
        return cls._instance
    
    def set(self, key, value):
        self.settings[key] = value
    
    def get(self, key, default=None):
        return self.settings.get(key, default)

# N'importe où dans l'application
config = AppConfig()
config.set('database_url', 'postgresql://localhost/mydb')
config.set('debug', True)

# Dans un autre fichier
config2 = AppConfig()
print(config2.get('database_url'))  # postgresql://localhost/mydb


[OK] 2. FACTORY METHOD - "Déléguer la création d'objets"

POURQUOI?
- Créer des objets sans spécifier leur classe exacte
- Le code appelant ne connaît pas les détails de création
- Facilite l'ajout de nouveaux types

QUAND?
- Plusieurs types d'objets similaires à créer
- Le type exact n'est connu qu'à l'exécution
- Besoin de centraliser la logique de création

COMMENT?

# === Exemple: Créer différents types de documents ===

from abc import ABC, abstractmethod

# Interface commune
class Document(ABC):
    @abstractmethod
    def open(self):
        pass
    
    @abstractmethod
    def save(self):
        pass

# Implémentations concrètes
class PDFDocument(Document):
    def open(self):
        return "Ouverture du PDF"
    
    def save(self):
        return "Sauvegarde du PDF"

class WordDocument(Document):
    def open(self):
        return "Ouverture du Word"
    
    def save(self):
        return "Sauvegarde du Word"

class ExcelDocument(Document):
    def open(self):
        return "Ouverture de l'Excel"
    
    def save(self):
        return "Sauvegarde de l'Excel"

# Factory Method
class DocumentFactory:
    @staticmethod
    def create_document(doc_type):
        if doc_type == "pdf":
            return PDFDocument()
        elif doc_type == "word":
            return WordDocument()
        elif doc_type == "excel":
            return ExcelDocument()
        else:
            raise ValueError(f"Type de document inconnu: {doc_type}")

# Utilisation
factory = DocumentFactory()

doc1 = factory.create_document("pdf")
print(doc1.open())   # Ouverture du PDF

doc2 = factory.create_document("word")
print(doc2.save())   # Sauvegarde du Word


# === Exemple: Créer des connexions de base de données ===

class DatabaseConnection(ABC):
    @abstractmethod
    def connect(self):
        pass
    
    @abstractmethod
    def query(self, sql):
        pass

class MySQLConnection(DatabaseConnection):
    def connect(self):
        return "Connecté à MySQL"
    
    def query(self, sql):
        return f"MySQL: Exécution de {sql}"

class PostgreSQLConnection(DatabaseConnection):
    def connect(self):
        return "Connecté à PostgreSQL"
    
    def query(self, sql):
        return f"PostgreSQL: Exécution de {sql}"

class SQLiteConnection(DatabaseConnection):
    def connect(self):
        return "Connecté à SQLite"
    
    def query(self, sql):
        return f"SQLite: Exécution de {sql}"

# Factory
class DBConnectionFactory:
    @staticmethod
    def get_connection(db_type):
        connections = {
            'mysql': MySQLConnection,
            'postgresql': PostgreSQLConnection,
            'sqlite': SQLiteConnection
        }
        
        connection_class = connections.get(db_type.lower())
        if connection_class:
            return connection_class()
        raise ValueError(f"Base de données non supportée: {db_type}")

# Utilisation
db = DBConnectionFactory.get_connection('postgresql')
print(db.connect())  # Connecté à PostgreSQL
print(db.query("SELECT * FROM users"))


# === Exemple avec enregistrement dynamique ===

class ShapeFactory:
    _shapes = {}
    
    @classmethod
    def register_shape(cls, name, shape_class):
        cls._shapes[name] = shape_class
    
    @classmethod
    def create_shape(cls, name):
        shape_class = cls._shapes.get(name)
        if not shape_class:
            raise ValueError(f"Shape inconnue: {name}")
        return shape_class()

# Classes de formes
class Circle:
    def draw(self):
        return "Dessin d'un cercle"

class Square:
    def draw(self):
        return "Dessin d'un carré"

class Triangle:
    def draw(self):
        return "Dessin d'un triangle"

# Enregistrement
ShapeFactory.register_shape('circle', Circle)
ShapeFactory.register_shape('square', Square)
ShapeFactory.register_shape('triangle', Triangle)

# Utilisation
shape1 = ShapeFactory.create_shape('circle')
print(shape1.draw())  # Dessin d'un cercle


[OK] 3. ABSTRACT FACTORY - "Famille de Factory Methods"

POURQUOI?
- Créer des familles d'objets liés sans spécifier leurs classes
- Garantir que les objets d'une même famille sont utilisés ensemble
- Exemple: Thèmes UI (boutons, fenêtres du même style)

QUAND?
- Plusieurs familles de produits liés
- Les produits doivent être cohérents entre eux
- Besoin de changer toute une famille d'objets en une fois

COMMENT?

# === Exemple: Thèmes d'interface graphique ===

from abc import ABC, abstractmethod

# Produits abstraits
class Button(ABC):
    @abstractmethod
    def render(self):
        pass

class Checkbox(ABC):
    @abstractmethod
    def render(self):
        pass

# Produits concrets - Style Windows
class WindowsButton(Button):
    def render(self):
        return "Rendu d'un bouton Windows"

class WindowsCheckbox(Checkbox):
    def render(self):
        return "Rendu d'une checkbox Windows"

# Produits concrets - Style macOS
class MacOSButton(Button):
    def render(self):
        return "Rendu d'un bouton macOS"

class MacOSCheckbox(Checkbox):
    def render(self):
        return "Rendu d'une checkbox macOS"

# Produits concrets - Style Linux
class LinuxButton(Button):
    def render(self):
        return "Rendu d'un bouton Linux"

class LinuxCheckbox(Checkbox):
    def render(self):
        return "Rendu d'une checkbox Linux"

# Factory abstraite
class GUIFactory(ABC):
    @abstractmethod
    def create_button(self):
        pass
    
    @abstractmethod
    def create_checkbox(self):
        pass

# Factories concrètes
class WindowsFactory(GUIFactory):
    def create_button(self):
        return WindowsButton()
    
    def create_checkbox(self):
        return WindowsCheckbox()

class MacOSFactory(GUIFactory):
    def create_button(self):
        return MacOSButton()
    
    def create_checkbox(self):
        return MacOSCheckbox()

class LinuxFactory(GUIFactory):
    def create_button(self):
        return LinuxButton()
    
    def create_checkbox(self):
        return LinuxCheckbox()

# Application cliente
class Application:
    def __init__(self, factory: GUIFactory):
        self.factory = factory
    
    def create_ui(self):
        button = self.factory.create_button()
        checkbox = self.factory.create_checkbox()
        
        print(button.render())
        print(checkbox.render())

# Utilisation
import platform

def get_factory():
    system = platform.system()
    if system == 'Windows':
        return WindowsFactory()
    elif system == 'Darwin':  # macOS
        return MacOSFactory()
    else:
        return LinuxFactory()

factory = get_factory()
app = Application(factory)
app.create_ui()


# === Exemple: Restaurant avec cuisines différentes ===

# Produits abstraits
class Pizza(ABC):
    @abstractmethod
    def prepare(self):
        pass

class Pasta(ABC):
    @abstractmethod
    def cook(self):
        pass

# Produits italiens
class ItalianPizza(Pizza):
    def prepare(self):
        return "Pizza italienne authentique"

class ItalianPasta(Pasta):
    def cook(self):
        return "Pasta fraîche maison"

# Produits américains
class AmericanPizza(Pizza):
    def prepare(self):
        return "Pizza américaine épaisse"

class AmericanPasta(Pasta):
    def cook(self):
        return "Mac and cheese"

# Factories
class ItalianRestaurantFactory(ABC):
    def create_pizza(self):
        return ItalianPizza()
    
    def create_pasta(self):
        return ItalianPasta()

class AmericanRestaurantFactory(ABC):
    def create_pizza(self):
        return AmericanPizza()
    
    def create_pasta(self):
        return AmericanPasta()

# Utilisation
class Restaurant:
    def __init__(self, factory):
        self.factory = factory
    
    def serve_meal(self):
        pizza = self.factory.create_pizza()
        pasta = self.factory.create_pasta()
        
        print(pizza.prepare())
        print(pasta.cook())

# Client choisit le type de restaurant
italian = Restaurant(ItalianRestaurantFactory())
italian.serve_meal()


[OK] 4. BUILDER - "Construire des objets complexes étape par étape"

POURQUOI?
- Créer des objets complexes avec beaucoup de paramètres
- Éviter des constructeurs avec trop de paramètres
- Permettre différentes représentations du même objet

QUAND?
- Objet avec beaucoup d'attributs optionnels
- Processus de création en plusieurs étapes
- Besoin de créer différentes versions d'un objet

COMMENT?

# === Exemple: Construire une maison ===

class House:
    def __init__(self):
        self.foundation = None
        self.structure = None
        self.roof = None
        self.interior = None
        self.pool = False
        self.garage = False
        self.garden = False
    
    def __str__(self):
        return f"""
        Maison:
        - Fondation: {self.foundation}
        - Structure: {self.structure}
        - Toit: {self.roof}
        - Intérieur: {self.interior}
        - Piscine: {self.pool}
        - Garage: {self.garage}
        - Jardin: {self.garden}
        """

class HouseBuilder:
    def __init__(self):
        self.house = House()
    
    def set_foundation(self, foundation):
        self.house.foundation = foundation
        return self  # Permet le chaînage!
    
    def set_structure(self, structure):
        self.house.structure = structure
        return self
    
    def set_roof(self, roof):
        self.house.roof = roof
        return self
    
    def set_interior(self, interior):
        self.house.interior = interior
        return self
    
    def add_pool(self):
        self.house.pool = True
        return self
    
    def add_garage(self):
        self.house.garage = True
        return self
    
    def add_garden(self):
        self.house.garden = True
        return self
    
    def build(self):
        return self.house

# Utilisation - Chaînage de méthodes
builder = HouseBuilder()
house = (builder
         .set_foundation("Béton")
         .set_structure("Brique")
         .set_roof("Tuiles")
         .set_interior("Moderne")
         .add_pool()
         .add_garage()
         .build())

print(house)


# === Exemple: Construire une requête SQL ===

class SQLQuery:
    def __init__(self):
        self.select_clause = []
        self.from_clause = None
        self.where_conditions = []
        self.order_by = []
        self.limit_value = None
    
    def __str__(self):
        query = f"SELECT {', '.join(self.select_clause)}"
        query += f" FROM {self.from_clause}"
        
        if self.where_conditions:
            query += f" WHERE {' AND '.join(self.where_conditions)}"
        
        if self.order_by:
            query += f" ORDER BY {', '.join(self.order_by)}"
        
        if self.limit_value:
            query += f" LIMIT {self.limit_value}"
        
        return query

class SQLQueryBuilder:
    def __init__(self):
        self.query = SQLQuery()
    
    def select(self, *columns):
        self.query.select_clause.extend(columns)
        return self
    
    def from_table(self, table):
        self.query.from_clause = table
        return self
    
    def where(self, condition):
        self.query.where_conditions.append(condition)
        return self
    
    def order_by(self, *columns):
        self.query.order_by.extend(columns)
        return self
    
    def limit(self, value):
        self.query.limit_value = value
        return self
    
    def build(self):
        return self.query

# Utilisation
query = (SQLQueryBuilder()
         .select('id', 'name', 'email')
         .from_table('users')
         .where('age > 18')
         .where("status = 'active'")
         .order_by('name')
         .limit(10)
         .build())

print(query)  # SELECT id, name, email FROM users WHERE age > 18 AND status = 'active' ORDER BY name LIMIT 10


# === Exemple: Construire un email ===

class Email:
    def __init__(self):
        self.sender = None
        self.recipients = []
        self.cc = []
        self.bcc = []
        self.subject = None
        self.body = None
        self.attachments = []
    
    def __str__(self):
        return f"""
        Email:
        De: {self.sender}
        À: {', '.join(self.recipients)}
        CC: {', '.join(self.cc)}
        Sujet: {self.subject}
        Corps: {self.body}
        Pièces jointes: {len(self.attachments)}
        """

class EmailBuilder:
    def __init__(self):
        self.email = Email()
    
    def from_sender(self, sender):
        self.email.sender = sender
        return self
    
    def to(self, *recipients):
        self.email.recipients.extend(recipients)
        return self
    
    def cc(self, *recipients):
        self.email.cc.extend(recipients)
        return self
    
    def bcc(self, *recipients):
        self.email.bcc.extend(recipients)
        return self
    
    def subject(self, subject):
        self.email.subject = subject
        return self
    
    def body(self, body):
        self.email.body = body
        return self
    
    def attach(self, filename):
        self.email.attachments.append(filename)
        return self
    
    def build(self):
        return self.email

# Utilisation
email = (EmailBuilder()
         .from_sender('john@example.com')
         .to('alice@example.com', 'bob@example.com')
         .cc('manager@example.com')
         .subject('Réunion importante')
         .body('Bonjour, la réunion est confirmée pour demain.')
         .attach('agenda.pdf')
         .attach('presentation.pptx')
         .build())

print(email)


# === Builder avec Director (patron complet) ===

class Car:
    def __init__(self):
        self.parts = []
    
    def add(self, part):
        self.parts.append(part)
    
    def show(self):
        print("Voiture avec:", ', '.join(self.parts))

class CarBuilder:
    def __init__(self):
        self.car = Car()
    
    def add_engine(self):
        pass
    
    def add_wheels(self):
        pass
    
    def add_doors(self):
        pass
    
    def get_car(self):
        return self.car

class SportsCarBuilder(CarBuilder):
    def add_engine(self):
        self.car.add("Moteur V8")
        return self
    
    def add_wheels(self):
        self.car.add("Roues sport 20 pouces")
        return self
    
    def add_doors(self):
        self.car.add("2 portes")
        return self

class FamilyCarBuilder(CarBuilder):
    def add_engine(self):
        self.car.add("Moteur économique")
        return self
    
    def add_wheels(self):
        self.car.add("Roues standard 16 pouces")
        return self
    
    def add_doors(self):
        self.car.add("4 portes")
        return self

# Director - Contrôle le processus de construction
class CarDirector:
    def __init__(self, builder):
        self.builder = builder
    
    def construct_car(self):
        return (self.builder
                .add_engine()
                .add_wheels()
                .add_doors()
                .get_car())

# Utilisation
sports_builder = SportsCarBuilder()
director = CarDirector(sports_builder)
sports_car = director.construct_car()
sports_car.show()  # Voiture avec: Moteur V8, Roues sport 20 pouces, 2 portes


[OK] 5. PROTOTYPE - "Cloner des objets existants"

POURQUOI?
- Créer de nouveaux objets en copiant des objets existants
- Éviter la création coûteuse d'objets complexes
- Créer des objets sans connaître leur classe exacte

QUAND?
- Création d'objet coûteuse (bases de données, fichiers, réseau)
- Objets avec beaucoup d'états similaires
- Besoin de cloner des objets sans dépendre de leurs classes

COMMENT?

# === Exemple simple avec copy ===

import copy

class Prototype:
    def clone(self):
        return copy.deepcopy(self)

class Person(Prototype):
    def __init__(self, name, age, address):
        self.name = name
        self.age = age
        self.address = address  # Objet mutable
    
    def __str__(self):
        return f"{self.name}, {self.age} ans, habite à {self.address}"

# Utilisation
original = Person("Alice", 30, {"ville": "Paris", "rue": "Champs-Élysées"})
print("Original:", original)

# Clone superficiel (shallow copy) - ATTENTION!
clone1 = copy.copy(original)
clone1.name = "Bob"
clone1.address["ville"] = "Lyon"  # Modifie aussi l'original!
print("Original après clone1:", original)  # Paris devient Lyon!

# Clone profond (deep copy) - CORRECT
clone2 = copy.deepcopy(original)
clone2.name = "Charlie"
clone2.address["ville"] = "Marseille"
print("Original après clone2:", original)  # Reste Lyon
print("Clone2:", clone2)  # Marseille


# === Exemple: Prototype de documents ===

class Document(Prototype):
    def __init__(self, title, content, formatting):
        self.title = title
        self.content = content
        self.formatting = formatting.copy()  # Dict de style
    
    def __str__(self):
        return f"Document: {self.title}\n{self.content}\nStyle: {self.formatting}"

# Document template
template = Document(
    title="Rapport Mensuel",
    content="",
    formatting={
        'font': 'Arial',
        'size': 12,
        'color': 'black',
        'margins': (2, 2, 2, 2)
    }
)

# Créer documents à partir du template
doc1 = copy.deepcopy(template)
doc1.title = "Rapport Janvier"
doc1.content = "Ventes: +15%"

doc2 = copy.deepcopy(template)
doc2.title = "Rapport Février"
doc2.content = "Ventes: +20%"

print(doc1)
print("\n", doc2)


# === Exemple: Registry de prototypes ===

class PrototypeRegistry:
    def __init__(self):
        self._prototypes = {}
    
    def register(self, name, prototype):
        self._prototypes[name] = prototype
    
    def unregister(self, name):
        del self._prototypes[name]
    
    def clone(self, name):
        prototype = self._prototypes.get(name)
        if not prototype:
            raise ValueError(f"Prototype '{name}' non trouvé")
        return copy.deepcopy(prototype)

# Classe de forme
class Shape:
    def __init__(self, color, border_width):
        self.color = color
        self.border_width = border_width
    
    def __str__(self):
        return f"{self.__class__.__name__}: {self.color}, bordure {self.border_width}px"

class Circle(Shape):
    def __init__(self, color, border_width, radius):
        super().__init__(color, border_width)
        self.radius = radius
    
    def __str__(self):
        return f"{super().__str__()}, rayon {self.radius}"

class Rectangle(Shape):
    def __init__(self, color, border_width, width, height):
        super().__init__(color, border_width)
        self.width = width
        self.height = height
    
    def __str__(self):
        return f"{super().__str__()}, {self.width}x{self.height}"

# Créer un registry avec des prototypes
registry = PrototypeRegistry()
registry.register('red-circle', Circle('red', 2, 10))
registry.register('blue-rect', Rectangle('blue', 1, 20, 30))

# Cloner des prototypes
circle1 = registry.clone('red-circle')
circle1.radius = 15
print(circle1)  # Circle: red, bordure 2px, rayon 15

circle2 = registry.clone('red-circle')
print(circle2)  # Circle: red, bordure 2px, rayon 10 (original)


# === Exemple: Configuration complexe ===

class DatabaseConfig:
    def __init__(self):
        self.host = "localhost"
        self.port = 5432
        self.database = "mydb"
        self.username = "user"
        self.password = "pass"
        self.pool_size = 10
        self.timeout = 30
        self.ssl_config = {
            'enabled': False,
            'cert_path': None
        }
    
    def clone(self):
        return copy.deepcopy(self)
    
    def __str__(self):
        return f"DB Config: {self.username}@{self.host}:{self.port}/{self.database}"

# Configuration de base
base_config = DatabaseConfig()

# Créer config pour production
prod_config = base_config.clone()
prod_config.host = "prod.example.com"
prod_config.pool_size = 50
prod_config.ssl_config['enabled'] = True

# Créer config pour développement
dev_config = base_config.clone()
dev_config.database = "mydb_dev"
dev_config.pool_size = 5

print("Production:", prod_config)
print("Développement:", dev_config)
print("Base (inchangée):", base_config)


# === Exemple avec __copy__ et __deepcopy__ personnalisés ===

class CustomObject:
    def __init__(self, value, nested_list):
        self.value = value
        self.nested_list = nested_list
        self._cache = {}  # Ne pas copier le cache
    
    def __copy__(self):
        # Copie superficielle personnalisée
        new_obj = CustomObject(self.value, self.nested_list)
        # Cache n'est pas copié
        return new_obj
    
    def __deepcopy__(self, memo):
        # Copie profonde personnalisée
        new_obj = CustomObject(
            copy.deepcopy(self.value, memo),
            copy.deepcopy(self.nested_list, memo)
        )
        # Cache n'est toujours pas copié
        return new_obj
    
    def __str__(self):
        return f"Value: {self.value}, List: {self.nested_list}, Cache size: {len(self._cache)}"

# Utilisation
original = CustomObject(42, [[1, 2], [3, 4]])
original._cache['key'] = 'value'

cloned = copy.deepcopy(original)
print("Original:", original)  # Cache size: 1
print("Cloned:", cloned)      # Cache size: 0 (pas copié!)


╔════════════════════════════════════════════════════════════════════╗
║                    PATTERNS STRUCTURELS (7)                        ║
╚════════════════════════════════════════════════════════════════════╝


[OK] 6. ADAPTER - "Convertir une interface en une autre"

POURQUOI?
- Faire fonctionner ensemble des classes incompatibles
- Adapter du code existant sans le modifier
- Utiliser une bibliothèque avec une interface différente

QUAND?
- Intégration de code tiers
- Réutilisation de classe existante avec interface incompatible
- Besoin de plusieurs implémentations interchangeables

COMMENT?

# === Exemple: Adapter différents formats de paiement ===

# Interface cible (ce que le client attend)
class PaymentProcessor:
    def process_payment(self, amount):
        raise NotImplementedError

# Classe existante incompatible 1
class PayPalAPI:
    def send_payment(self, amount_in_usd):
        return f"PayPal: Paiement de ${amount_in_usd} effectué"

# Classe existante incompatible 2
class StripeAPI:
    def make_charge(self, cents):
        return f"Stripe: Charge de {cents} cents effectuée"

# Classe existante incompatible 3
class BitcoinAPI:
    def transfer(self, btc_amount):
        return f"Bitcoin: Transfert de {btc_amount} BTC effectué"

# Adapters pour chaque API
class PayPalAdapter(PaymentProcessor):
    def __init__(self):
        self.paypal = PayPalAPI()
    
    def process_payment(self, amount):
        # Adapte l'interface: amount en dollars
        return self.paypal.send_payment(amount)

class StripeAdapter(PaymentProcessor):
    def __init__(self):
        self.stripe = StripeAPI()
    
    def process_payment(self, amount):
        # Adapte: convertit dollars en cents
        cents = int(amount * 100)
        return self.stripe.make_charge(cents)

class BitcoinAdapter(PaymentProcessor):
    def __init__(self):
        self.bitcoin = BitcoinAPI()
    
    def process_payment(self, amount):
        # Adapte: convertit dollars en BTC (taux fictif)
        btc = amount / 50000
        return self.bitcoin.transfer(btc)

# Client utilise l'interface uniforme
class OnlineStore:
    def __init__(self, payment_processor: PaymentProcessor):
        self.payment_processor = payment_processor
    
    def checkout(self, amount):
        print(f"Total à payer: ${amount}")
        result = self.payment_processor.process_payment(amount)
        print(result)
        print("-" * 50)

# Utilisation - Toutes les méthodes de paiement ont la même interface!
store = OnlineStore(PayPalAdapter())
store.checkout(100)  # PayPal: Paiement de $100 effectué

store = OnlineStore(StripeAdapter())
store.checkout(100)  # Stripe: Charge de 10000 cents effectuée

store = OnlineStore(BitcoinAdapter())
store.checkout(100)  # Bitcoin: Transfert de 0.002 BTC effectué


# === Exemple: Adapter ancienne API de base de données ===

# Nouvelle interface standard
class ModernDatabase:
    def connect(self, connection_string):
        raise NotImplementedError
    
    def execute(self, query):
        raise NotImplementedError
    
    def close(self):
        raise NotImplementedError

# Ancienne API legacy
class LegacyDatabase:
    def __init__(self):
        self.connected = False
    
    def open_connection(self, host, port, user, password, db):
        self.connected = True
        return f"Connexion ouverte à {host}:{port}/{db}"
    
    def run_sql(self, sql_statement):
        if not self.connected:
            return "Erreur: Pas connecté"
        return f"Exécution: {sql_statement}"
    
    def disconnect(self):
        self.connected = False
        return "Déconnexion réussie"

# Adapter pour l'ancienne API
class LegacyDatabaseAdapter(ModernDatabase):
    def __init__(self):
        self.legacy_db = LegacyDatabase()
    
    def connect(self, connection_string):
        # Parse: "user:pass@host:port/database"
        parts = connection_string.replace('@', ':').replace('/', ':').split(':')
        user, password, host, port, db = parts
        return self.legacy_db.open_connection(host, int(port), user, password, db)
    
    def execute(self, query):
        return self.legacy_db.run_sql(query)
    
    def close(self):
        return self.legacy_db.disconnect()

# Utilisation
db = LegacyDatabaseAdapter()
print(db.connect("admin:secret@localhost:5432/mydb"))
print(db.execute("SELECT * FROM users"))
print(db.close())


# === Exemple: Adapter format de données ===

import json
import xml.etree.ElementTree as ET

# Interface cible
class DataParser:
    def parse(self, data):
        raise NotImplementedError

# Parser JSON existant
class JSONParser:
    def parse_json(self, json_string):
        return json.loads(json_string)

# Parser XML existant
class XMLParser:
    def parse_xml(self, xml_string):
        root = ET.fromstring(xml_string)
        result = {}
        for child in root:
            result[child.tag] = child.text
        return result

# Adapters
class JSONAdapter(DataParser):
    def __init__(self):
        self.parser = JSONParser()
    
    def parse(self, data):
        return self.parser.parse_json(data)

class XMLAdapter(DataParser):
    def __init__(self):
        self.parser = XMLParser()
    
    def parse(self, data):
        return self.parser.parse_xml(data)

# Client
class DataProcessor:
    def __init__(self, parser: DataParser):
        self.parser = parser
    
    def process(self, data):
        parsed = self.parser.parse(data)
        print("Données parsées:", parsed)

# Utilisation
json_data = '{"name": "Alice", "age": 30}'
xml_data = '<person><name>Bob</name><age>25</age></person>'

processor = DataProcessor(JSONAdapter())
processor.process(json_data)

processor = DataProcessor(XMLAdapter())
processor.process(xml_data)


[OK] 7. BRIDGE - "Séparer abstraction et implémentation"

POURQUOI?
- Séparer l'abstraction de son implémentation
- Les deux peuvent varier indépendamment
- Éviter l'explosion de sous-classes

QUAND?
- Besoin de varier abstraction ET implémentation
- Éviter lien permanent entre abstraction et implémentation
- Partager implémentation entre plusieurs objets

COMMENT?

# === Exemple: Formes avec différents rendus ===

# Implémentation (le "comment")
class Renderer:
    def render_circle(self, radius):
        raise NotImplementedError
    
    def render_square(self, side):
        raise NotImplementedError

class VectorRenderer(Renderer):
    def render_circle(self, radius):
        return f"Dessin vectoriel d'un cercle de rayon {radius}"
    
    def render_square(self, side):
        return f"Dessin vectoriel d'un carré de côté {side}"

class RasterRenderer(Renderer):
    def render_circle(self, radius):
        return f"Dessin pixels d'un cercle de rayon {radius}"
    
    def render_square(self, side):
        return f"Dessin pixels d'un carré de côté {side}"

class OpenGLRenderer(Renderer):
    def render_circle(self, radius):
        return f"Rendu OpenGL d'un cercle de rayon {radius}"
    
    def render_square(self, side):
        return f"Rendu OpenGL d'un carré de côté {side}"

# Abstraction (le "quoi")
class Shape:
    def __init__(self, renderer: Renderer):
        self.renderer = renderer
    
    def draw(self):
        raise NotImplementedError

class Circle(Shape):
    def __init__(self, renderer: Renderer, radius):
        super().__init__(renderer)
        self.radius = radius
    
    def draw(self):
        return self.renderer.render_circle(self.radius)

class Square(Shape):
    def __init__(self, renderer: Renderer, side):
        super().__init__(renderer)
        self.side = side
    
    def draw(self):
        return self.renderer.render_square(self.side)

# Utilisation - Combiner n'importe quelle forme avec n'importe quel rendu
vector = VectorRenderer()
raster = RasterRenderer()
opengl = OpenGLRenderer()

# Cercle vectoriel
circle1 = Circle(vector, 5)
print(circle1.draw())  # Dessin vectoriel d'un cercle de rayon 5

# Cercle raster
circle2 = Circle(raster, 5)
print(circle2.draw())  # Dessin pixels d'un cercle de rayon 5

# Carré OpenGL
square = Square(opengl, 10)
print(square.draw())  # Rendu OpenGL d'un carré de côté 10


# === Exemple: Télécommandes et appareils ===

# Implémentation
class Device:
    def turn_on(self):
        raise NotImplementedError
    
    def turn_off(self):
        raise NotImplementedError
    
    def set_volume(self, volume):
        raise NotImplementedError

class TV(Device):
    def __init__(self):
        self.on = False
        self.volume = 10
    
    def turn_on(self):
        self.on = True
        return "TV allumée"
    
    def turn_off(self):
        self.on = False
        return "TV éteinte"
    
    def set_volume(self, volume):
        self.volume = volume
        return f"Volume TV: {volume}"

class Radio(Device):
    def __init__(self):
        self.on = False
        self.volume = 5
    
    def turn_on(self):
        self.on = True
        return "Radio allumée"
    
    def turn_off(self):
        self.on = False
        return "Radio éteinte"
    
    def set_volume(self, volume):
        self.volume = volume
        return f"Volume Radio: {volume}"

# Abstraction
class RemoteControl:
    def __init__(self, device: Device):
        self.device = device
    
    def toggle_power(self):
        if hasattr(self.device, 'on') and self.device.on:
            return self.device.turn_off()
        return self.device.turn_on()
    
    def volume_up(self):
        current = self.device.volume
        return self.device.set_volume(current + 1)
    
    def volume_down(self):
        current = self.device.volume
        return self.device.set_volume(current - 1)

class AdvancedRemoteControl(RemoteControl):
    def mute(self):
        return self.device.set_volume(0)

# Utilisation
tv = TV()
tv_remote = RemoteControl(tv)
print(tv_remote.toggle_power())  # TV allumée
print(tv_remote.volume_up())     # Volume TV: 11

radio = Radio()
advanced_remote = AdvancedRemoteControl(radio)
print(advanced_remote.toggle_power())  # Radio allumée
print(advanced_remote.mute())          # Volume Radio: 0


# === Exemple: Messages avec différents envois ===

# Implémentation
class MessageSender:
    def send(self, recipient, message):
        raise NotImplementedError

class EmailSender(MessageSender):
    def send(self, recipient, message):
        return f"Email à {recipient}: {message}"

class SMSSender(MessageSender):
    def send(self, recipient, message):
        return f"SMS à {recipient}: {message}"

class PushNotificationSender(MessageSender):
    def send(self, recipient, message):
        return f"Notification push à {recipient}: {message}"

# Abstraction
class Message:
    def __init__(self, sender: MessageSender):
        self.sender = sender
    
    def send(self, recipient, content):
        raise NotImplementedError

class TextMessage(Message):
    def send(self, recipient, content):
        return self.sender.send(recipient, content)

class UrgentMessage(Message):
    def send(self, recipient, content):
        urgent_content = f"[URGENT] {content}"
        return self.sender.send(recipient, urgent_content)

class EncryptedMessage(Message):
    def send(self, recipient, content):
        encrypted = f"[ENCRYPTED: {content[::-1]}]"  # Simple reverse
        return self.sender.send(recipient, encrypted)

# Utilisation
email = EmailSender()
sms = SMSSender()

# Message texte par email
msg1 = TextMessage(email)
print(msg1.send("alice@example.com", "Bonjour!"))

# Message urgent par SMS
msg2 = UrgentMessage(sms)
print(msg2.send("+33612345678", "Réunion annulée"))

# Message crypté par push
push = PushNotificationSender()
msg3 = EncryptedMessage(push)
print(msg3.send("user123", "Code secret: 1234"))


[OK] 8. COMPOSITE - "Arbre d'objets comme un seul objet"

POURQUOI?
- Traiter objets individuels et compositions uniformément
- Créer des structures arborescentes
- Exemple: système de fichiers, menus, organisations

QUAND?
- Structure hiérarchique d'objets
- Client doit traiter objets simples et composés de la même façon
- Structure "partie-tout"

COMMENT?

# === Exemple: Système de fichiers ===

class FileSystemComponent:
    def __init__(self, name):
        self.name = name
    
    def display(self, indent=0):
        raise NotImplementedError
    
    def get_size(self):
        raise NotImplementedError

# Feuille (élément individuel)
class File(FileSystemComponent):
    def __init__(self, name, size):
        super().__init__(name)
        self.size = size
    
    def display(self, indent=0):
        print("  " * indent + f"[FICHIER] {self.name} ({self.size} KB)")
    
    def get_size(self):
        return self.size

# Composite (contient d'autres éléments)
class Directory(FileSystemComponent):
    def __init__(self, name):
        super().__init__(name)
        self.children = []
    
    def add(self, component):
        self.children.append(component)
        return self
    
    def remove(self, component):
        self.children.remove(component)
    
    def display(self, indent=0):
        print("  " * indent + f"[DOSSIER] {self.name}/")
        for child in self.children:
            child.display(indent + 1)
    
    def get_size(self):
        return sum(child.get_size() for child in self.children)

# Utilisation
root = Directory("root")
home = Directory("home")
user = Directory("alice")
documents = Directory("documents")

file1 = File("photo.jpg", 2048)
file2 = File("resume.pdf", 512)
file3 = File("notes.txt", 64)

# Construire l'arbre
root.add(home)
home.add(user)
user.add(documents)
documents.add(file1)
documents.add(file2)
user.add(file3)

# Afficher
root.display()
# [DOSSIER] root/
#   [DOSSIER] home/
#     [DOSSIER] alice/
#       [DOSSIER] documents/
#         [FICHIER] photo.jpg (2048 KB)
#         [FICHIER] resume.pdf (512 KB)
#       [FICHIER] notes.txt (64 KB)

print(f"\nTaille totale: {root.get_size()} KB")  # 2624 KB


# === Exemple: Organisation d'entreprise ===

class Employee:
    def __init__(self, name, position, salary):
        self.name = name
        self.position = position
        self.salary = salary
    
    def show_details(self, indent=0):
        print("  " * indent + f"[UTILISATEUR] {self.name} - {self.position} (${self.salary}/mois)")
    
    def get_salary_cost(self):
        return self.salary

class Department:
    def __init__(self, name):
        self.name = name
        self.members = []
    
    def add(self, member):
        self.members.append(member)
        return self
    
    def remove(self, member):
        self.members.remove(member)
    
    def show_details(self, indent=0):
        print("  " * indent + f"[ENTREPRISE] Département: {self.name}")
        for member in self.members:
            member.show_details(indent + 1)
    
    def get_salary_cost(self):
        return sum(member.get_salary_cost() for member in self.members)

# Utilisation
company = Department("Entreprise XYZ")

it_dept = Department("IT")
it_dept.add(Employee("Alice", "Dev Senior", 5000))
it_dept.add(Employee("Bob", "Dev Junior", 3000))

sales_dept = Department("Ventes")
sales_dept.add(Employee("Charlie", "Commercial Senior", 4500))
sales_dept.add(Employee("Diana", "Commercial", 3500))

company.add(it_dept)
company.add(sales_dept)
company.add(Employee("Eve", "PDG", 10000))

company.show_details()
print(f"\nCoût salarial total: ${company.get_salary_cost()}/mois")


# === Exemple: Menu de restaurant ===

class MenuComponent:
    def __init__(self, name, description=""):
        self.name = name
        self.description = description
    
    def display(self, indent=0):
        raise NotImplementedError
    
    def get_price(self):
        return 0

class MenuItem(MenuComponent):
    def __init__(self, name, description, price):
        super().__init__(name, description)
        self.price = price
    
    def display(self, indent=0):
        print("  " * indent + f"• {self.name} - ${self.price}")
        if self.description:
            print("  " * indent + f"  ({self.description})")
    
    def get_price(self):
        return self.price

class MenuCategory(MenuComponent):
    def __init__(self, name, description=""):
        super().__init__(name, description)
        self.items = []
    
    def add(self, item):
        self.items.append(item)
        return self
    
    def display(self, indent=0):
        print("  " * indent + f"═══ {self.name.upper()} ═══")
        if self.description:
            print("  " * indent + f"{self.description}")
        print()
        for item in self.items:
            item.display(indent)
        print()
    
    def get_price(self):
        return sum(item.get_price() for item in self.items)

# Utilisation
menu = MenuCategory("Menu Principal")

entrees = MenuCategory("Entrées", "Pour bien commencer")
entrees.add(MenuItem("Salade César", "Laitue, poulet, parmesan", 8.50))
entrees.add(MenuItem("Soupe du jour", "Demandez au serveur", 6.00))

plats = MenuCategory("Plats Principaux")
plats.add(MenuItem("Steak frites", "Viande de qualité premium", 22.00))
plats.add(MenuItem("Saumon grillé", "Avec légumes de saison", 24.00))
plats.add(MenuItem("Pâtes carbonara", "Recette traditionnelle", 16.00))

desserts = MenuCategory("Desserts", "La touche sucrée")
desserts.add(MenuItem("Tiramisu", "Fait maison", 7.00))
desserts.add(MenuItem("Tarte citron", "Meringuée", 6.50))

menu.add(entrees)
menu.add(plats)
menu.add(desserts)

menu.display()


# === Exemple: Graphiques (formes groupées) ===

class Graphic:
    def draw(self):
        raise NotImplementedError
    
    def move(self, x, y):
        raise NotImplementedError

class Circle(Graphic):
    def __init__(self, x, y, radius):
        self.x = x
        self.y = y
        self.radius = radius
    
    def draw(self):
        print(f"Cercle à ({self.x}, {self.y}), rayon {self.radius}")
    
    def move(self, x, y):
        self.x += x
        self.y += y

class Rectangle(Graphic):
    def __init__(self, x, y, width, height):
        self.x = x
        self.y = y
        self.width = width
        self.height = height
    
    def draw(self):
        print(f"Rectangle à ({self.x}, {self.y}), {self.width}x{self.height}")
    
    def move(self, x, y):
        self.x += x
        self.y += y

class GraphicGroup(Graphic):
    def __init__(self):
        self.graphics = []
    
    def add(self, graphic):
        self.graphics.append(graphic)
        return self
    
    def remove(self, graphic):
        self.graphics.remove(graphic)
    
    def draw(self):
        print("--- Groupe de graphiques ---")
        for graphic in self.graphics:
            graphic.draw()
        print("---------------------------")
    
    def move(self, x, y):
        for graphic in self.graphics:
            graphic.move(x, y)

# Utilisation
circle = Circle(10, 10, 5)
rect = Rectangle(20, 20, 30, 40)

group = GraphicGroup()
group.add(circle)
group.add(rect)

# Sous-groupe
subgroup = GraphicGroup()
subgroup.add(Circle(50, 50, 10))
subgroup.add(Circle(60, 60, 10))

# Groupe principal contient tout
main_group = GraphicGroup()
main_group.add(group)
main_group.add(subgroup)

main_group.draw()
print("\nDéplacement de +10, +10")
main_group.move(10, 10)
main_group.draw()


[OK] 9. DECORATOR - "Ajouter des fonctionnalités dynamiquement"

POURQUOI?
- Ajouter des responsabilités à un objet dynamiquement
- Alternative flexible à l'héritage
- Combiner plusieurs comportements

QUAND?
- Ajouter fonctionnalités sans modifier la classe
- Fonctionnalités optionnelles et combinables
- Trop de sous-classes avec héritage

COMMENT?

# === Exemple: Décorateurs de café ===

# Composant de base
class Coffee:
    def get_description(self):
        return "Café"
    
    def get_cost(self):
        return 2.00

# Décorateurs
class CoffeeDecorator:
    def __init__(self, coffee):
        self._coffee = coffee
    
    def get_description(self):
        return self._coffee.get_description()
    
    def get_cost(self):
        return self._coffee.get_cost()

class Milk(CoffeeDecorator):
    def get_description(self):
        return self._coffee.get_description() + ", Lait"
    
    def get_cost(self):
        return self._coffee.get_cost() + 0.50

class Sugar(CoffeeDecorator):
    def get_description(self):
        return self._coffee.get_description() + ", Sucre"
    
    def get_cost(self):
        return self._coffee.get_cost() + 0.20

class WhippedCream(CoffeeDecorator):
    def get_description(self):
        return self._coffee.get_description() + ", Crème fouettée"
    
    def get_cost(self):
        return self._coffee.get_cost() + 0.70

class Caramel(CoffeeDecorator):
    def get_description(self):
        return self._coffee.get_description() + ", Caramel"
    
    def get_cost(self):
        return self._coffee.get_cost() + 0.60

# Utilisation - Emballage successif
coffee = Coffee()
print(f"{coffee.get_description()}: ${coffee.get_cost()}")
# Café: $2.0

coffee_with_milk = Milk(coffee)
print(f"{coffee_with_milk.get_description()}: ${coffee_with_milk.get_cost()}")
# Café, Lait: $2.5

fancy_coffee = Caramel(WhippedCream(Milk(Sugar(Coffee()))))
print(f"{fancy_coffee.get_description()}: ${fancy_coffee.get_cost()}")
# Café, Sucre, Lait, Crème fouettée, Caramel: $4.0


# === Exemple: Notifications ===

class Notifier:
    def send(self, message):
        return f"Envoi: {message}"

class NotifierDecorator:
    def __init__(self, notifier):
        self._notifier = notifier
    
    def send(self, message):
        return self._notifier.send(message)

class EmailNotifier(NotifierDecorator):
    def send(self, message):
        base = self._notifier.send(message)
        return base + "\n  -> Email envoyé"

class SMSNotifier(NotifierDecorator):
    def send(self, message):
        base = self._notifier.send(message)
        return base + "\n  -> SMS envoyé"

class SlackNotifier(NotifierDecorator):
    def send(self, message):
        base = self._notifier.send(message)
        return base + "\n  -> Message Slack envoyé"

class FacebookNotifier(NotifierDecorator):
    def send(self, message):
        base = self._notifier.send(message)
        return base + "\n  -> Post Facebook publié"

# Utilisation
notifier = Notifier()

# Notification simple
print(notifier.send("Bonjour!"))
print("\n" + "="*50 + "\n")

# Multi-canal
multi_notifier = FacebookNotifier(
    SlackNotifier(
        SMSNotifier(
            EmailNotifier(notifier)
        )
    )
)

print(multi_notifier.send("Alerte importante!"))
# Envoi: Alerte importante!
#   -> Email envoyé
#   -> SMS envoyé
#   -> Message Slack envoyé
#   -> Post Facebook publié


# === Exemple: Compression et chiffrement de données ===

class DataSource:
    def write_data(self, data):
        print(f"Écriture: {data}")
    
    def read_data(self):
        return "données brutes"

class DataSourceDecorator:
    def __init__(self, source):
        self._source = source
    
    def write_data(self, data):
        self._source.write_data(data)
    
    def read_data(self):
        return self._source.read_data()

class CompressionDecorator(DataSourceDecorator):
    def write_data(self, data):
        compressed = self._compress(data)
        super().write_data(compressed)
    
    def read_data(self):
        data = super().read_data()
        return self._decompress(data)
    
    def _compress(self, data):
        return f"[COMPRESSED: {data}]"
    
    def _decompress(self, data):
        return data.replace("[COMPRESSED: ", "").replace("]", "")

class EncryptionDecorator(DataSourceDecorator):
    def write_data(self, data):
        encrypted = self._encrypt(data)
        super().write_data(encrypted)
    
    def read_data(self):
        data = super().read_data()
        return self._decrypt(data)
    
    def _encrypt(self, data):
        # Simple reverse pour l'exemple
        return f"[ENCRYPTED: {data[::-1]}]"
    
    def _decrypt(self, data):
        content = data.replace("[ENCRYPTED: ", "").replace("]", "")
        return content[::-1]

# Utilisation
source = DataSource()

# Avec compression
compressed_source = CompressionDecorator(source)
compressed_source.write_data("Hello World")
# Écriture: [COMPRESSED: Hello World]

# Avec compression ET chiffrement
secure_source = EncryptionDecorator(CompressionDecorator(source))
secure_source.write_data("Secret Data")
# Écriture: [ENCRYPTED: ]atad terceS :DESSERPMONC[[]


# === Exemple avec décorateurs Python natifs ===

def uppercase_decorator(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper

def exclamation_decorator(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result + "!!!"
    return wrapper

@exclamation_decorator
@uppercase_decorator
def greet(name):
    return f"Bonjour {name}"

print(greet("Alice"))  # BONJOUR ALICE!!!


# === Exemple: Logger avec décorateurs ===

import time
import functools

def timer_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} a pris {end - start:.4f} secondes")
        return result
    return wrapper

def log_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Appel de {func.__name__} avec args={args}, kwargs={kwargs}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} a retourné: {result}")
        return result
    return wrapper

@timer_decorator
@log_decorator
def calculate_sum(n):
    time.sleep(0.1)  # Simulation
    return sum(range(n))

result = calculate_sum(100)


[OK] 10. FACADE - "Interface simplifiée pour système complexe"

POURQUOI?
- Simplifier l'utilisation d'un système complexe
- Fournir une interface unifiée
- Réduire les dépendances externes

QUAND?
- Système complexe difficile à utiliser
- Besoin d'interface simple pour débutants
- Découpler client d'un sous-système
- Créer des couches dans l'architecture

COMMENT?

# === Exemple: Home Cinema ===

# Sous-systèmes complexes
class Amplifier:
    def on(self):
        return "Amplificateur allumé"
    
    def off(self):
        return "Amplificateur éteint"
    
    def set_volume(self, level):
        return f"Volume: {level}"

class DVDPlayer:
    def on(self):
        return "Lecteur DVD allumé"
    
    def off(self):
        return "Lecteur DVD éteint"
    
    def play(self, movie):
        return f"Lecture de '{movie}'"
    
    def stop(self):
        return "Lecture arrêtée"

class Projector:
    def on(self):
        return "Projecteur allumé"
    
    def off(self):
        return "Projecteur éteint"
    
    def wide_screen_mode(self):
        return "Mode écran large activé"

class Lights:
    def dim(self, level):
        return f"Lumières réduites à {level}%"
    
    def on(self):
        return "Lumières allumées"

class Screen:
    def down(self):
        return "Écran abaissé"
    
    def up(self):
        return "Écran relevé"

# FACADE - Interface simplifiée
class HomeTheaterFacade:
    def __init__(self):
        self.amp = Amplifier()
        self.dvd = DVDPlayer()
        self.projector = Projector()
        self.lights = Lights()
        self.screen = Screen()
    
    def watch_movie(self, movie):
        print("Préparation du cinéma maison...")
        print(self.lights.dim(10))
        print(self.screen.down())
        print(self.projector.on())
        print(self.projector.wide_screen_mode())
        print(self.amp.on())
        print(self.amp.set_volume(5))
        print(self.dvd.on())
        print(self.dvd.play(movie))
        print("Bon film!\n")
    
    def end_movie(self):
        print("Arrêt du cinéma maison...")
        print(self.dvd.stop())
        print(self.dvd.off())
        print(self.amp.off())
        print(self.projector.off())
        print(self.screen.up())
        print(self.lights.on())
        print("Cinéma éteint!\n")

# Utilisation - SIMPLE!
theater = HomeTheaterFacade()
theater.watch_movie("The Matrix")
theater.end_movie()


# === Exemple: API de conversion de devises ===

import requests

# Sous-systèmes complexes
class CurrencyConverter:
    def get_rate(self, from_curr, to_curr):
        # Appel API complexe
        return 1.18  # Simulé

class CurrencyValidator:
    VALID_CURRENCIES = ['USD', 'EUR', 'GBP', 'JPY']
    
    def is_valid(self, currency):
        return currency.upper() in self.VALID_CURRENCIES

class CurrencyFormatter:
    SYMBOLS = {'USD': ', 'EUR': '€', 'GBP': '£', 'JPY': '¥'}
    
    def format(self, amount, currency):
        symbol = self.SYMBOLS.get(currency, '')
        return f"{symbol}{amount:.2f}"

# FACADE
class CurrencyExchangeFacade:
    def __init__(self):
        self.converter = CurrencyConverter()
        self.validator = CurrencyValidator()
        self.formatter = CurrencyFormatter()
    
    def convert(self, amount, from_curr, to_curr):
        # Validation
        if not self.validator.is_valid(from_curr):
            return f"Erreur: Devise invalide {from_curr}"
        if not self.validator.is_valid(to_curr):
            return f"Erreur: Devise invalide {to_curr}"
        
        # Conversion
        rate = self.converter.get_rate(from_curr, to_curr)
        converted = amount * rate
        
        # Formatage
        result = self.formatter.format(converted, to_curr)
        return f"{amount} {from_curr} = {result}"

# Utilisation
exchange = CurrencyExchangeFacade()
print(exchange.convert(100, 'EUR', 'USD'))  # 100 EUR = $118.00


# === Exemple: Système de commande en ligne ===

# Sous-systèmes
class Inventory:
    def check_stock(self, product_id):
        return True  # Simulé
    
    def reserve(self, product_id, quantity):
        print(f"  -> {quantity}x produit {product_id} réservé")

class Payment:
    def process_payment(self, amount, card_number):
        print(f"  -> Paiement de ${amount} traité")
        return True

class Shipping:
    def calculate_shipping(self, address):
        return 10.00
    
    def schedule_delivery(self, address):
        print(f"  -> Livraison programmée vers {address}")

class Notification:
    def send_confirmation(self, email, order_id):
        print(f"  -> Email de confirmation envoyé à {email}")

# FACADE
class OrderFacade:
    def __init__(self):
        self.inventory = Inventory()
        self.payment = Payment()
        self.shipping = Shipping()
        self.notification = Notification()
    
    def place_order(self, customer_email, product_id, quantity, 
                    address, card_number):
        print("Traitement de la commande...")
        
        # 1. Vérifier stock
        if not self.inventory.check_stock(product_id):
            return "Erreur: Produit en rupture de stock"
        
        # 2. Réserver produit
        self.inventory.reserve(product_id, quantity)
        
        # 3. Calculer total
        product_price = 50.00  # Simulé
        shipping_cost = self.shipping.calculate_shipping(address)
        total = (product_price * quantity) + shipping_cost
        
        # 4. Traiter paiement
        if not self.payment.process_payment(total, card_number):
            return "Erreur: Paiement refusé"
        
        # 5. Programmer livraison
        self.shipping.schedule_delivery(address)
        
        # 6. Envoyer confirmation
        order_id = "ORD-12345"
        self.notification.send_confirmation(customer_email, order_id)
        
        return f"Commande {order_id} confirmée!"

# Utilisation
order_system = OrderFacade()
result = order_system.place_order(
    customer_email="alice@example.com",
    product_id="PROD-001",
    quantity=2,
    address="123 Rue Example, Paris",
    card_number="1234-5678-9012-3456"
)
print(result)


[OK] 11. FLYWEIGHT - "Partager des données pour économiser mémoire"

POURQUOI?
- Réduire l'utilisation de mémoire
- Partager des données communes entre objets
- Gérer efficacement un grand nombre d'objets

QUAND?
- Application utilise beaucoup d'objets similaires
- Coût mémoire élevé
- La plupart des états peuvent être externes

COMMENT?

# === Exemple: Caractères dans un éditeur de texte ===

class CharacterFlyweight:
    def __init__(self, char, font, size):
        # État intrinsèque (partagé)
        self.char = char
        self.font = font
        self.size = size
    
    def display(self, color, position):
        # État extrinsèque (unique par usage)
        print(f"'{self.char}' [{self.font}, {self.size}pt] "
              f"à position {position} en {color}")

class CharacterFactory:
    _flyweights = {}
    
    @classmethod
    def get_character(cls, char, font, size):
        key = (char, font, size)
        if key not in cls._flyweights:
            cls._flyweights[key] = CharacterFlyweight(char, font, size)
            print(f"  [Création de flyweight pour '{char}']")
        return cls._flyweights[key]
    
    @classmethod
    def get_flyweight_count(cls):
        return len(cls._flyweights)

# Utilisation
factory = CharacterFactory()

# Document avec répétitions
text = "HELLO WORLD"
colors = ['black'] * len(text)

print("Affichage du texte:")
for i, char in enumerate(text):
    flyweight = factory.get_character(char, 'Arial', 12)
    flyweight.display(colors[i], i)

print(f"\nNombre total de flyweights créés: {factory.get_flyweight_count()}")
print(f"Sans flyweight, on aurait créé: {len(text)} objets")
print(f"Économie: {len(text) - factory.get_flyweight_count()} objets")


# === Exemple: Arbres dans un jeu ===

class TreeType:
    def __init__(self, name, color, texture):
        # État intrinsèque (partagé entre tous les arbres du même type)
        self.name = name
        self.color = color
        self.texture = texture
        print(f"  [TreeType '{name}' créé en mémoire]")
    
    def draw(self, canvas, x, y):
        print(f"Dessin d'un {self.name} {self.color} à ({x}, {y})")

class TreeFactory:
    _tree_types = {}
    
    @classmethod
    def get_tree_type(cls, name, color, texture):
        key = (name, color, texture)
        if key not in cls._tree_types:
            cls._tree_types[key] = TreeType(name, color, texture)
        return cls._tree_types[key]
    
    @classmethod
    def report(cls):
        count = len(cls._tree_types)
        print(f"\n[TreeFactory] {count} types d'arbres en mémoire")

class Tree:
    def __init__(self, x, y, tree_type):
        # État extrinsèque (unique par arbre)
        self.x = x
        self.y = y
        self.tree_type = tree_type
    
    def draw(self, canvas):
        self.tree_type.draw(canvas, self.x, self.y)

# Simulation d'une forêt
class Forest:
    def __init__(self):
        self.trees = []
    
    def plant_tree(self, x, y, name, color, texture):
        tree_type = TreeFactory.get_tree_type(name, color, texture)
        tree = Tree(x, y, tree_type)
        self.trees.append(tree)
    
    def draw(self, canvas):
        for tree in self.trees:
            tree.draw(canvas)

# Utilisation
print("Plantation de la forêt:")
forest = Forest()

# Planter 1000 chênes (mais un seul TreeType en mémoire!)
for i in range(10):
    forest.plant_tree(i, i*2, "Chêne", "vert", "oak.png")

# Planter 500 pins
for i in range(5):
    forest.plant_tree(i*3, i, "Pin", "vert foncé", "pine.png")

TreeFactory.report()
print(f"\nNombre total d'arbres: {len(forest.trees)}")
print("Mémoire économisée: Au lieu de 15 objets complets, "
      "seulement 2 TreeType + 15 références légères!")

print("\nAffichage de quelques arbres:")
forest.trees[0].draw("canvas")
forest.trees[10].draw("canvas")


# === Exemple: Particules dans un jeu ===

import random

class ParticleType:
    def __init__(self, color, sprite):
        self.color = color
        self.sprite = sprite
    
    def draw(self, x, y, velocity):
        print(f"Particule {self.color} à ({x:.1f}, {y:.1f}), "
              f"vitesse {velocity:.1f}")

class ParticleFactory:
    _types = {}
    
    @classmethod
    def get_particle_type(cls, color, sprite):
        key = (color, sprite)
        if key not in cls._types:
            cls._types[key] = ParticleType(color, sprite)
        return cls._types[key]

class Particle:
    def __init__(self, x, y, velocity, particle_type):
        self.x = x
        self.y = y
        self.velocity = velocity
        self.type = particle_type
    
    def move(self, dt):
        self.x += self.velocity * dt
        self.y += self.velocity * dt
    
    def draw(self):
        self.type.draw(self.x, self.y, self.velocity)

# Système de particules
class ParticleSystem:
    def __init__(self):
        self.particles = []
    
    def emit(self, x, y, color, sprite, count):
        for _ in range(count):
            particle_type = ParticleFactory.get_particle_type(color, sprite)
            velocity = random.uniform(1.0, 5.0)
            self.particles.append(Particle(x, y, velocity, particle_type))
    
    def update(self, dt):
        for particle in self.particles:
            particle.move(dt)
    
    def draw(self):
        for particle in self.particles[:3]:  # Afficher 3 premiers
            particle.draw()

# Utilisation
system = ParticleSystem()

# Créer explosion de particules rouges
system.emit(100, 100, "red", "star.png", 1000)

# Créer fumée bleue
system.emit(200, 200, "blue", "smoke.png", 500)

print(f"Particules créées: {len(system.particles)}")
print("Affichage de quelques particules:")
system.draw()


[OK] 12. PROXY - "Contrôler l'accès à un objet"

POURQUOI?
- Contrôler l'accès à un objet
- Ajouter une couche intermédiaire
- Lazy loading, caching, logging, sécurité

QUAND?
- Objet coûteux à créer (créer à la demande)
- Contrôle d'accès nécessaire
- Objet distant (réseau)
- Logging des opérations

COMMENT?

# === Exemple: Protection Proxy (contrôle d'accès) ===

from abc import ABC, abstractmethod

class Document(ABC):
    @abstractmethod
    def display(self):
        pass
    
    @abstractmethod
    def edit(self, content):
        pass

class RealDocument(Document):
    def __init__(self, filename):
        self.filename = filename
        self.content = f"Contenu de {filename}"
        print(f"  [Document '{filename}' chargé en mémoire]")
    
    def display(self):
        return self.content
    
    def edit(self, content):
        self.content = content
        return f"Document modifié: {content}"

class DocumentProxy(Document):
    def __init__(self, filename, user_role):
        self.filename = filename
        self.user_role = user_role
        self._real_document = None
    
    def display(self):
        # Tout le monde peut lire
        if self._real_document is None:
            self._real_document = RealDocument(self.filename)
        return self._real_document.display()
    
    def edit(self, content):
        # Seuls les admins peuvent modifier
        if self.user_role != "admin":
            return "ERREUR: Accès refusé. Droits admin requis."
        
        if self._real_document is None:
            self._real_document = RealDocument(self.filename)
        return self._real_document.edit(content)

# Utilisation
print("=== Utilisateur normal ===")
doc_user = DocumentProxy("secret.txt", "user")
print(doc_user.display())  # OK
print(doc_user.edit("Nouveau contenu"))  # REFUSÉ

print("\n=== Administrateur ===")
doc_admin = DocumentProxy("secret.txt", "admin")
print(doc_admin.display())  # OK
print(doc_admin.edit("Nouveau contenu"))  # OK


# === Exemple: Virtual Proxy (lazy loading) ===

class ExpensiveObject:
    def __init__(self, data_file):
        print(f"  [Chargement LOURD de {data_file}...]")
        import time
        time.sleep(1)  # Simulation chargement lourd
        self.data = f"Données de {data_file}"
        print(f"  [Chargement terminé]")
    
    def process(self):
        return f"Traitement: {self.data}"

class LazyProxy:
    def __init__(self, data_file):
        self.data_file = data_file
        self._real_object = None
    
    def process(self):
        # Créé uniquement quand nécessaire
        if self._real_object is None:
            print("  [Proxy: Création de l'objet réel...]")
            self._real_object = ExpensiveObject(self.data_file)
        return self._real_object.process()

# Utilisation
print("Création du proxy (rapide):")
proxy = LazyProxy("big_data.dat")
print("Proxy créé!\n")

print("Première utilisation (lent - chargement):")
print(proxy.process())

print("\nDeuxième utilisation (rapide - déjà chargé):")
print(proxy.process())


# === Exemple: Caching Proxy ===

class Database:
    def query(self, sql):
        print(f"  [BD: Exécution de la requête...]")
        import time
        time.sleep(0.5)  # Simulation requête lente
        return f"Résultats pour: {sql}"

class CachingDatabaseProxy:
    def __init__(self):
        self.database = Database()
        self.cache = {}
    
    def query(self, sql):
        if sql in self.cache:
            print("  [Proxy: Résultat en cache!]")
            return self.cache[sql]
        
        print("  [Proxy: Cache miss, requête à la BD...]")
        result = self.database.query(sql)
        self.cache[sql] = result
        return result
    
    def clear_cache(self):
        self.cache.clear()
        print("  [Proxy: Cache vidé]")

# Utilisation
db = CachingDatabaseProxy()

print("Première requête:")
print(db.query("SELECT * FROM users"))

print("\nDeuxième requête (même SQL):")
print(db.query("SELECT * FROM users"))  # Instantané!

print("\nRequête différente:")
print(db.query("SELECT * FROM products"))


# === Exemple: Logging Proxy ===

import datetime

class Service:
    def operation(self, param):
        return f"Opération effectuée avec {param}"

class LoggingProxy:
    def __init__(self, service):
        self.service = service
        self.log = []
    
    def operation(self, param):
        # Log avant
        timestamp = datetime.datetime.now()
        self.log.append({
            'time': timestamp,
            'method': 'operation',
            'param': param
        })
        
        # Exécution
        result = self.service.operation(param)
        
        # Log après
        print(f"[LOG {timestamp}] operation({param}) -> {result}")
        
        return result
    
    def show_logs(self):
        print("\n=== HISTORIQUE ===")
        for entry in self.log:
            print(f"{entry['time']}: {entry['method']}({entry['param']})")

# Utilisation
service = Service()
proxy = LoggingProxy(service)

proxy.operation("test1")
proxy.operation("test2")
proxy.operation("test3")

proxy.show_logs()


# === Exemple: Remote Proxy (API distante) ===

class RemoteAPI:
    def fetch_data(self, endpoint):
        print(f"  [API: Requête HTTP à {endpoint}...]")
        import time
        time.sleep(0.3)
        return {"data": f"Résultats de {endpoint}", "status": 200}

class RemoteAPIProxy:
    def __init__(self):
        self.api = RemoteAPI()
        self.cache = {}
        self.request_count = 0
        self.max_requests = 5
    
    def fetch_data(self, endpoint):
        # Rate limiting
        if self.request_count >= self.max_requests:
            return {"error": "Limite de requêtes atteinte", "status": 429}
        
        # Cache
        if endpoint in self.cache:
            print("  [Proxy: Donnée en cache]")
            return self.cache[endpoint]
        
        # Requête réelle
        self.request_count += 1
        result = self.api.fetch_data(endpoint)
        self.cache[endpoint] = result
        
        return result
    
    def reset_counter(self):
        self.request_count = 0

# Utilisation
api = RemoteAPIProxy()

print("Requête 1:")
print(api.fetch_data("/users"))

print("\nRequête 2 (même endpoint):")
print(api.fetch_data("/users"))

print(f"\nRequêtes effectuées: {api.request_count}/5")


╔════════════════════════════════════════════════════════════════════╗
║                  PATTERNS COMPORTEMENTAUX (11)                     ║
╚════════════════════════════════════════════════════════════════════╝


[OK] 13. CHAIN OF RESPONSIBILITY - "Chaîne de handlers"

POURQUOI?
- Passer une requête le long d'une chaîne de handlers
- Découpler émetteur et récepteur
- Plusieurs objets peuvent traiter la requête

QUAND?
- Plusieurs objets peuvent traiter une requête
- Handler n'est pas connu à l'avance
- Traitement en plusieurs étapes

COMMENT?

# === Exemple: Support client par niveau ===

class SupportHandler:
    def __init__(self):
        self.next_handler = None
    
    def set_next(self, handler):
        self.next_handler = handler
        return handler
    
    def handle(self, request):
        if self.next_handler:
            return self.next_handler.handle(request)
        return "Aucun handler disponible"

class Level1Support(SupportHandler):
    def handle(self, request):
        if request['priority'] == 'low':
            return f"Level 1: Résolution de '{request['issue']}'"
        return super().handle(request)

class Level2Support(SupportHandler):
    def handle(self, request):
        if request['priority'] == 'medium':
            return f"Level 2: Résolution de '{request['issue']}'"
        return super().handle(request)

class Level3Support(SupportHandler):
    def handle(self, request):
        if request['priority'] == 'high':
            return f"Level 3: Escalade de '{request['issue']}'"
        return super().handle(request)

# Construction de la chaîne
level1 = Level1Support()
level2 = Level2Support()
level3 = Level3Support()

level1.set_next(level2).set_next(level3)

# Utilisation
requests = [
    {'issue': 'Password reset', 'priority': 'low'},
    {'issue': 'Bug in app', 'priority': 'medium'},
    {'issue': 'Server down', 'priority': 'high'},
]

for req in requests:
    print(level1.handle(req))


# === Exemple: Pipeline de validation ===

class Validator:
    def __init__(self):
        self.next_validator = None
    
    def set_next(self, validator):
        self.next_validator = validator
        return validator
    
    def validate(self, data):
        if self.next_validator:
            return self.next_validator.validate(data)
        return True

class NotEmptyValidator(Validator):
    def validate(self, data):
        if not data or not data.strip():
            return "Erreur: Le champ ne peut pas être vide"
        return super().validate(data)

class LengthValidator(Validator):
    def __init__(self, min_length, max_length):
        super().__init__()
        self.min_length = min_length
        self.max_length = max_length
    
    def validate(self, data):
        if len(data) < self.min_length:
            return f"Erreur: Minimum {self.min_length} caractères"
        if len(data) > self.max_length:
            return f"Erreur: Maximum {self.max_length} caractères"
        return super().validate(data)

class EmailFormatValidator(Validator):
    def validate(self, data):
        if '@' not in data or '.' not in data:
            return "Erreur: Format email invalide"
        return super().validate(data)

# Construction du pipeline
empty_check = NotEmptyValidator()
length_check = LengthValidator(5, 50)
email_check = EmailFormatValidator()

empty_check.set_next(length_check).set_next(email_check)

# Utilisation
test_emails = [
    "",
    "abc",
    "test",
    "invalidemail",
    "valid@email.com"
]

for email in test_emails:
    result = empty_check.validate(email)
    status = "[OK]" if result == True else "[X]"
    print(f"{status} '{email}': {result if result != True else 'Valide'}")


# === Exemple: Middleware HTTP ===

class Middleware:
    def __init__(self):
        self.next = None
    
    def link_with(self, next_middleware):
        self.next = next_middleware
        return next_middleware
    
    def check(self, request):
        if self.next:
            return self.next.check(request)
        return True

class AuthenticationMiddleware(Middleware):
    def check(self, request):
        if 'token' not in request:
            return "Erreur 401: Token manquant"
        if request['token'] != 'valid_token':
            return "Erreur 401: Token invalide"
        print("  [OK] Authentication OK")
        return super().check(request)

class RateLimitMiddleware(Middleware):
    def __init__(self, max_requests=3):
        super().__init__()
        self.max_requests = max_requests
        self.request_count = 0
    
    def check(self, request):
        self.request_count += 1
        if self.request_count > self.max_requests:
            return "Erreur 429: Trop de requêtes"
        print(f"  [OK] Rate limit OK ({self.request_count}/{self.max_requests})")
        return super().check(request)

class ValidationMiddleware(Middleware):
    def check(self, request):
        if 'data' not in request:
            return "Erreur 400: Données manquantes"
        print("  [OK] Validation OK")
        return super().check(request)

# Construction de la chaîne
auth = AuthenticationMiddleware()
rate_limit = RateLimitMiddleware(3)
validation = ValidationMiddleware()

auth.link_with(rate_limit).link_with(validation)

# Utilisation
requests = [
    {'token': 'valid_token', 'data': {'name': 'Alice'}},
    {'token': 'valid_token', 'data': {'name': 'Bob'}},
    {'token': 'invalid_token', 'data': {'name': 'Charlie'}},
    {'token': 'valid_token'},  # Pas de data
]

for i, req in enumerate(requests):
    print(f"\nRequête {i+1}:")
    result = auth.check(req)
    print(f"Résultat: {result}")


[OK] 14. COMMAND - "Encapsuler une action en objet"

POURQUOI?
- Transformer une requête en objet autonome
- Paramétrer objets avec des opérations
- Implémenter undo/redo
- Logger, mettre en queue des opérations

QUAND?
- Besoin d'undo/redo
- Queue d'opérations
- Logging des opérations
- Macro-commandes (combinaisons)

COMMENT?

# === Exemple: Éditeur de texte avec undo/redo ===

from abc import ABC, abstractmethod

# Receiver
class TextEditor:
    def __init__(self):
        self.text = ""
    
    def write(self, text):
        self.text += text
    
    def delete(self, length):
        self.text = self.text[:-length]
    
    def get_text(self):
        return self.text

# Command Interface
class Command(ABC):
    @abstractmethod
    def execute(self):
        pass
    
    @abstractmethod
    def undo(self):
        pass

# Concrete Commands
class WriteCommand(Command):
    def __init__(self, editor, text):
        self.editor = editor
        self.text = text
    
    def execute(self):
        self.editor.write(self.text)
    
    def undo(self):
        self.editor.delete(len(self.text))

class DeleteCommand(Command):
    def __init__(self, editor, length):
        self.editor = editor
        self.length = length
        self.deleted_text = ""
    
    def execute(self):
        # Sauvegarder le texte supprimé
        self.deleted_text = self.editor.get_text()[-self.length:]
        self.editor.delete(self.length)
    
    def undo(self):
        self.editor.write(self.deleted_text)

# Invoker
class EditorInvoker:
    def __init__(self):
        self.history = []
        self.redo_stack = []
    
    def execute_command(self, command):
        command.execute()
        self.history.append(command)
        self.redo_stack.clear()
    
    def undo(self):
        if not self.history:
            return "Rien à annuler"
        command = self.history.pop()
        command.undo()
        self.redo_stack.append(command)
        return "Annulation effectuée"
    
    def redo(self):
        if not self.redo_stack:
            return "Rien à refaire"
        command = self.redo_stack.pop()
        command.execute()
        self.history.append(command)
        return "Refait"

# Utilisation
editor = TextEditor()
invoker = EditorInvoker()

# Écrire
write1 = WriteCommand(editor, "Bonjour ")
invoker.execute_command(write1)
print(f"Texte: '{editor.get_text()}'")

write2 = WriteCommand(editor, "le monde!")
invoker.execute_command(write2)
print(f"Texte: '{editor.get_text()}'")

# Supprimer
delete1 = DeleteCommand(editor, 8)  # Supprimer "le monde"
invoker.execute_command(delete1)
print(f"Texte: '{editor.get_text()}'")

# Undo
invoker.undo()
print(f"Après undo: '{editor.get_text()}'")

invoker.undo()
print(f"Après undo: '{editor.get_text()}'")

# Redo
invoker.redo()
print(f"Après redo: '{editor.get_text()}'")


# === Exemple: Télécommande universelle ===

# Receivers
class Light:
    def __init__(self, location):
        self.location = location
        self.on = False
    
    def turn_on(self):
        self.on = True
        print(f"Lumière {self.location} allumée")
    
    def turn_off(self):
        self.on = False
        print(f"Lumière {self.location} éteinte")

class TV:
    def __init__(self):
        self.on = False
        self.volume = 10
    
    def turn_on(self):
        self.on = True
        print("TV allumée")
    
    def turn_off(self):
        self.on = False
        print("TV éteinte")
    
    def volume_up(self):
        self.volume += 1
        print(f"Volume: {self.volume}")
    
    def volume_down(self):
        self.volume -= 1
        print(f"Volume: {self.volume}")

# Commands
class LightOnCommand(Command):
    def __init__(self, light):
        self.light = light
    
    def execute(self):
        self.light.turn_on()
    
    def undo(self):
        self.light.turn_off()

class LightOffCommand(Command):
    def __init__(self, light):
        self.light = light
    
    def execute(self):
        self.light.turn_off()
    
    def undo(self):
        self.light.turn_on()

class TVOnCommand(Command):
    def __init__(self, tv):
        self.tv = tv
    
    def execute(self):
        self.tv.turn_on()
    
    def undo(self):
        self.tv.turn_off()

class TVVolumeUpCommand(Command):
    def __init__(self, tv):
        self.tv = tv
    
    def execute(self):
        self.tv.volume_up()
    
    def undo(self):
        self.tv.volume_down()

# Macro Command (combinaison)
class MacroCommand(Command):
    def __init__(self, commands):
        self.commands = commands
    
    def execute(self):
        for command in self.commands:
            command.execute()
    
    def undo(self):
        for command in reversed(self.commands):
            command.undo()

# Remote Control
class RemoteControl:
    def __init__(self):
        self.commands = {}
        self.history = []
    
    def set_command(self, button, command):
        self.commands[button] = command
    
    def press_button(self, button):
        if button in self.commands:
            command = self.commands[button]
            command.execute()
            self.history.append(command)
        else:
            print(f"Bouton {button} non configuré")
    
    def press_undo(self):
        if self.history:
            command = self.history.pop()
            command.undo()
        else:
            print("Rien à annuler")

# Utilisation
living_room_light = Light("salon")
bedroom_light = Light("chambre")
tv = TV()

remote = RemoteControl()

# Configuration des boutons
remote.set_command(1, LightOnCommand(living_room_light))
remote.set_command(2, LightOffCommand(living_room_light))
remote.set_command(3, TVOnCommand(tv))
remote.set_command(4, TVVolumeUpCommand(tv))

# Mode "Soirée" - macro command
party_mode = MacroCommand([
    LightOnCommand(living_room_light),
    LightOnCommand(bedroom_light),
    TVOnCommand(tv),
    TVVolumeUpCommand(tv),
    TVVolumeUpCommand(tv),
])
remote.set_command(9, party_mode)

# Tests
print("=== Test télécommande ===")
remote.press_button(1)  # Lumière salon ON
remote.press_button(3)  # TV ON
remote.press_button(4)  # Volume +

print("\n=== Undo ===")
remote.press_undo()  # Volume -
remote.press_undo()  # TV OFF

print("\n=== Mode soirée ===")
remote.press_button(9)  # Tout allumer!


# === Exemple: Transactions bancaires ===

class BankAccount:
    def __init__(self, balance=0):
        self.balance = balance
    
    def deposit(self, amount):
        self.balance += amount
        print(f"Dépôt de ${amount}, solde: ${self.balance}")
    
    def withdraw(self, amount):
        if self.balance >= amount:
            self.balance -= amount
            print(f"Retrait de ${amount}, solde: ${self.balance}")
            return True
        print(f"Solde insuffisant!")
        return False

class DepositCommand(Command):
    def __init__(self, account, amount):
        self.account = account
        self.amount = amount
    
    def execute(self):
        self.account.deposit(self.amount)
    
    def undo(self):
        self.account.withdraw(self.amount)

class WithdrawCommand(Command):
    def __init__(self, account, amount):
        self.account = account
        self.amount = amount
        self.success = False
    
    def execute(self):
        self.success = self.account.withdraw(self.amount)
    
    def undo(self):
        if self.success:
            self.account.deposit(self.amount)

class TransferCommand(Command):
    def __init__(self, from_account, to_account, amount):
        self.withdraw_cmd = WithdrawCommand(from_account, amount)
        self.deposit_cmd = DepositCommand(to_account, amount)
    
    def execute(self):
        self.withdraw_cmd.execute()
        if self.withdraw_cmd.success:
            self.deposit_cmd.execute()
    
    def undo(self):
        if self.withdraw_cmd.success:
            self.deposit_cmd.undo()
            self.withdraw_cmd.undo()

# Utilisation
account1 = BankAccount(1000)
account2 = BankAccount(500)

transfer = TransferCommand(account1, account2, 200)
transfer.execute()
# Retrait de $200, solde: $800
# Dépôt de $200, solde: $700

print("\nAnnulation du transfert:")
transfer.undo()
# Retrait de $200, solde: $500
# Dépôt de $200, solde: $1000


[OK] 15. ITERATOR - "Parcourir une collection"

POURQUOI?
- Accéder séquentiellement aux éléments sans exposer la structure
- Plusieurs façons de parcourir la même collection
- Interface uniforme pour différentes collections

QUAND?
- Masquer la complexité interne de la collection
- Plusieurs types de parcours
- Implémenter des collections personnalisées

COMMENT?

# === Exemple: Collection personnalisée ===

class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author
    
    def __str__(self):
        return f"'{self.title}' par {self.author}"

class BookCollection:
    def __init__(self):
        self._books = []
    
    def add_book(self, book):
        self._books.append(book)
    
    def __iter__(self):
        return BookIterator(self._books)
    
    def reverse_iterator(self):
        return ReverseBookIterator(self._books)
    
    def author_iterator(self, author):
        return AuthorFilterIterator(self._books, author)

class BookIterator:
    def __init__(self, books):
        self._books = books
        self._index = 0
    
    def __iter__(self):
        return self
    
    def __next__(self):
        if self._index < len(self._books):
            book = self._books[self._index]
            self._index += 1
            return book
        raise StopIteration

class ReverseBookIterator:
    def __init__(self, books):
        self._books = books
        self._index = len(books) - 1
    
    def __iter__(self):
        return self
    
    def __next__(self):
        if self._index >= 0:
            book = self._books[self._index]
            self._index -= 1
            return book
        raise StopIteration

class AuthorFilterIterator:
    def __init__(self, books, author):
        self._books = books
        self._author = author
        self._index = 0
    
    def __iter__(self):
        return self
    
    def __next__(self):
        while self._index < len(self._books):
            book = self._books[self._index]
            self._index += 1
            if book.author == self._author:
                return book
        raise StopIteration

# Utilisation
collection = BookCollection()
collection.add_book(Book("1984", "George Orwell"))
collection.add_book(Book("Animal Farm", "George Orwell"))
collection.add_book(Book("Brave New World", "Aldous Huxley"))
collection.add_book(Book("Fahrenheit 451", "Ray Bradbury"))

print("=== Parcours normal ===")
for book in collection:
    print(book)

print("\n=== Parcours inverse ===")
for book in collection.reverse_iterator():
    print(book)

print("\n=== Livres de George Orwell ===")
for book in collection.author_iterator("George Orwell"):
    print(book)


# === Exemple: Arbre binaire avec parcours ===

class TreeNode:
    def __init__(self, value):
        self.value = value
        self.left = None
        self.right = None

class BinaryTree:
    def __init__(self, root_value):
        self.root = TreeNode(root_value)
    
    def inorder_iterator(self):
        return InOrderIterator(self.root)
    
    def preorder_iterator(self):
        return PreOrderIterator(self.root)
    
    def postorder_iterator(self):
        return PostOrderIterator(self.root)

class InOrderIterator:
    def __init__(self, root):
        self.stack = []
        self.current = root
    
    def __iter__(self):
        return self
    
    def __next__(self):
        while self.current or self.stack:
            if self.current:
                self.stack.append(self.current)
                self.current = self.current.left
            else:
                self.current = self.stack.pop()
                value = self.current.value
                self.current = self.current.right
                return value
        raise StopIteration

class PreOrderIterator:
    def __init__(self, root):
        self.stack = [root] if root else []
    
    def __iter__(self):
        return self
    
    def __next__(self):
        if not self.stack:
            raise StopIteration
        
        node = self.stack.pop()
        if node.right:
            self.stack.append(node.right)
        if node.left:
            self.stack.append(node.left)
        
        return node.value

class PostOrderIterator:
    def __init__(self, root):
        self.stack = []
        self.last_visited = None
        self.current = root
    
    def __iter__(self):
        return self
    
    def __next__(self):
        while self.current or self.stack:
            if self.current:
                self.stack.append(self.current)
                self.current = self.current.left
            else:
                peek = self.stack[-1]
                if peek.right and self.last_visited != peek.right:
                    self.current = peek.right
                else:
                    value = peek.value
                    self.last_visited = self.stack.pop()
                    return value
        raise StopIteration

# Utilisation
tree = BinaryTree(5)
tree.root.left = TreeNode(3)
tree.root.right = TreeNode(8)
tree.root.left.left = TreeNode(1)
tree.root.left.right = TreeNode(4)

print("In-order (gauche-racine-droite):")
print(list(tree.inorder_iterator()))  # [1, 3, 4, 5, 8]

print("\nPre-order (racine-gauche-droite):")
print(list(tree.preorder_iterator()))  # [5, 3, 1, 4, 8]

print("\nPost-order (gauche-droite-racine):")
print(list(tree.postorder_iterator()))  # [1, 4, 3, 8, 5]


# === Exemple: Iterator avec générateur Python ===

class Range:
    def __init__(self, start, end, step=1):
        self.start = start
        self.end = end
        self.step = step
    
    def __iter__(self):
        current = self.start
        while current < self.end:
            yield current
            current += self.step

# Utilisation
my_range = Range(0, 10, 2)
for num in my_range:
    print(num, end=' ')  # 0 2 4 6 8

print("\n")

# Parcourir plusieurs fois
print("Premier parcours:", list(my_range))
print("Deuxième parcours:", list(my_range))


[OK] 16. MEDIATOR - "Centraliser la communication"

POURQUOI?
- Réduire les dépendances entre objets
- Centraliser la logique de communication
- Faciliter la maintenance

QUAND?
- Objets communiquent de manière complexe
- Réutilisation difficile à cause des dépendances
- Comportement distribué difficile à personnaliser

COMMENT?

# === Exemple: Chat room ===

class ChatMediator:
    def __init__(self):
        self.users = []
    
    def add_user(self, user):
        self.users.append(user)
    
    def send_message(self, message, sender):
        for user in self.users:
            if user != sender:
                user.receive(message, sender)

class User:
    def __init__(self, name, mediator):
        self.name = name
        self.mediator = mediator
        self.mediator.add_user(self)
    
    def send(self, message):
        print(f"\n{self.name} envoie: {message}")
        self.mediator.send_message(message, self)
    
    def receive(self, message, sender):
        print(f"{self.name} reçoit de {sender.name}: {message}")

# Utilisation
chat = ChatMediator()

alice = User("Alice", chat)
bob = User("Bob", chat)
charlie = User("Charlie", chat)

alice.send("Bonjour tout le monde!")
bob.send("Salut Alice!")


# === Exemple: Contrôle aérien ===

class AirTrafficControl:
    def __init__(self):
        self.airplanes = []
    
    def register_airplane(self, airplane):
        self.airplanes.append(airplane)
    
    def request_landing(self, airplane):
        print(f"\n[ATC] {airplane.name} demande atterrissage")
        
        # Vérifier autres avions
        for plane in self.airplanes:
            if plane != airplane and plane.status == "landing":
                return f"[ATC] à {airplane.name}: Patientez, piste occupée"
        
        airplane.status = "landing"
        return f"[ATC] à {airplane.name}: Autorisation accordée"
    
    def notify_landed(self, airplane):
        airplane.status = "landed"
        print(f"[ATC] {airplane.name} a atterri")

class Airplane:
    def __init__(self, name, atc):
        self.name = name
        self.atc = atc
        self.status = "flying"
        self.atc.register_airplane(self)
    
    def request_landing(self):
        response = self.atc.request_landing(self)
        print(response)
    
    def land(self):
        if self.status == "landing":
            print(f"{self.name} est en train d'atterrir...")
            self.atc.notify_landed(self)

# Utilisation
atc = AirTrafficControl()

flight1 = Airplane("Vol AA123", atc)
flight2 = Airplane("Vol BA456", atc)
flight3 = Airplane("Vol CA789", atc)

flight1.request_landing()
flight1.land()

flight2.request_landing()
flight3.request_landing()  # Devra attendre


# === Exemple: Interface graphique ===

class DialogMediator:
    def __init__(self):
        self.title = None
        self.text_box = None
        self.checkbox = None
        self.button = None
    
    def notify(self, sender, event):
        if sender == self.checkbox and event == "check":
            if self.checkbox.checked:
                self.text_box.enable()
                self.button.enable()
            else:
                self.text_box.disable()
                self.button.disable()
        
        elif sender == self.button and event == "click":
            text = self.text_box.get_text()
            print(f"Formulaire soumis: {text}")

class Component:
    def __init__(self, mediator):
        self.mediator = mediator

class TextBox(Component):
    def __init__(self, mediator):
        super().__init__(mediator)
        self.text = ""
        self.enabled = False
        self.mediator.text_box = self
    
    def set_text(self, text):
        if self.enabled:
            self.text = text
    
    def get_text(self):
        return self.text
    
    def enable(self):
        self.enabled = True
        print("TextBox activé")
    
    def disable(self):
        self.enabled = False
        print("TextBox désactivé")

class Checkbox(Component):
    def __init__(self, mediator):
        super().__init__(mediator)
        self.checked = False
        self.mediator.checkbox = self
    
    def toggle(self):
        self.checked = not self.checked
        print(f"Checkbox: {'cochée' if self.checked else 'décochée'}")
        self.mediator.notify(self, "check")

class Button(Component):
    def __init__(self, mediator):
        super().__init__(mediator)
        self.enabled = False
        self.mediator.button = self
    
    def click(self):
        if self.enabled:
            self.mediator.notify(self, "click")
    
    def enable(self):
        self.enabled = True
        print("Button activé")
    
    def disable(self):
        self.enabled = False
        print("Button désactivé")

# Utilisation
mediator = DialogMediator()
checkbox = Checkbox(mediator)
textbox = TextBox(mediator)
button = Button(mediator)

print("=== Cocher la checkbox ===")
checkbox.toggle()

print("\n=== Écrire du texte ===")
textbox.set_text("Hello World")

print("\n=== Cliquer sur le bouton ===")
button.click()


[OK] 17. MEMENTO - "Sauvegarder et restaurer l'état"

POURQUOI?
- Sauvegarder l'état interne d'un objet
- Restaurer un état précédent
- Ne pas violer l'encapsulation

QUAND?
- Implémenter undo/redo
- Sauvegarder des snapshots
- Restaurer l'état après erreur

COMMENT?

# === Exemple: Éditeur avec historique ===

class EditorMemento:
    def __init__(self, content, cursor_position):
        self._content = content
        self._cursor_position = cursor_position
    
    def get_content(self):
        return self._content
    
    def get_cursor_position(self):
        return self._cursor_position

class Editor:
    def __init__(self):
        self.content = ""
        self.cursor_position = 0
    
    def type(self, text):
        self.content += text
        self.cursor_position = len(self.content)
    
    def delete(self, length):
        self.content = self.content[:-length]
        self.cursor_position = len(self.content)
    
    def save(self):
        return EditorMemento(self.content, self.cursor_position)
    
    def restore(self, memento):
        self.content = memento.get_content()
        self.cursor_position = memento.get_cursor_position()
    
    def show(self):
        print(f"Contenu: '{self.content}' | Curseur: {self.cursor_position}")

class History:
    def __init__(self):
        self._mementos = []
    
    def save(self, memento):
        self._mementos.append(memento)
    
    def undo(self):
        if len(self._mementos) > 0:
            return self._mementos.pop()
        return None

# Utilisation
editor = Editor()
history = History()

editor.type("Bonjour ")
history.save(editor.save())
editor.show()

editor.type("le monde!")
history.save(editor.save())
editor.show()

editor.delete(8)
history.save(editor.save())
editor.show()

print("\n=== Undo ===")
memento = history.undo()
editor.restore(memento)
editor.show()

memento = history.undo()
editor.restore(memento)
editor.show()


# === Exemple: Jeu vidéo avec sauvegarde ===

class GameMemento:
    def __init__(self, level, health, position, inventory):
        self._level = level
        self._health = health
        self._position = position.copy()
        self._inventory = inventory.copy()
    
    def get_state(self):
        return {
            'level': self._level,
            'health': self._health,
            'position': self._position,
            'inventory': self._inventory
        }

class GameCharacter:
    def __init__(self):
        self.level = 1
        self.health = 100
        self.position = [0, 0]
        self.inventory = []
    
    def level_up(self):
        self.level += 1
        self.health = 100
        print(f"Level Up! Niveau {self.level}")
    
    def take_damage(self, damage):
        self.health -= damage
        print(f"Dégâts: -{damage} HP | Santé: {self.health}")
    
    def move(self, x, y):
        self.position = [x, y]
        print(f"Déplacement vers {self.position}")
    
    def add_item(self, item):
        self.inventory.append(item)
        print(f"Objet ramassé: {item}")
    
    def save_game(self):
        print("[SAUVEGARDE] Sauvegarde...")
        return GameMemento(self.level, self.health, self.position, self.inventory)
    
    def load_game(self, memento):
        print("[DOSSIER] Chargement...")
        state = memento.get_state()
        self.level = state['level']
        self.health = state['health']
        self.position = state['position']
        self.inventory = state['inventory']
    
    def show_status(self):
        print(f"\n{'='*40}")
        print(f"Niveau: {self.level} | HP: {self.health}")
        print(f"Position: {self.position}")
        print(f"Inventaire: {self.inventory}")
        print(f"{'='*40}\n")

# Utilisation
player = GameCharacter()
player.show_status()

# Jouer un peu
player.move(10, 5)
player.add_item("Épée")
player.level_up()
save1 = player.save_game()
player.show_status()

# Continuer
player.move(20, 15)
player.add_item("Potion")
player.take_damage(50)
save2 = player.save_game()
player.show_status()

# Boss fight - échec
player.take_damage(60)  # Mort!
player.show_status()

# Charger sauvegarde
print("=== Game Over! Chargement sauvegarde ===")
player.load_game(save2)
player.show_status()


# === Exemple: Configuration d'application ===

class ConfigMemento:
    def __init__(self, settings):
        self._settings = settings.copy()
    
    def get_settings(self):
        return self._settings.copy()

class AppConfig:
    def __init__(self):
        self.settings = {
            'theme': 'light',
            'language': 'fr',
            'notifications': True,
            'volume': 50
        }
    
    def set(self, key, value):
        self.settings[key] = value
        print(f"Configuration: {key} = {value}")
    
    def save(self):
        return ConfigMemento(self.settings)
    
    def restore(self, memento):
        self.settings = memento.get_settings()
        print("Configuration restaurée")
    
    def show(self):
        print("\nConfiguration actuelle:")
        for key, value in self.settings.items():
            print(f"  {key}: {value}")

class ConfigHistory:
    def __init__(self):
        self.history = []
    
    def backup(self, memento):
        self.history.append(memento)
    
    def get_backup(self, index):
        if 0 <= index < len(self.history):
            return self.history[index]
        return None

# Utilisation
config = AppConfig()
history = ConfigHistory()

config.show()
history.backup(config.save())

# Modifications
config.set('theme', 'dark')
config.set('volume', 75)
config.show()
history.backup(config.save())

# Plus de modifications
config.set('language', 'en')
config.set('notifications', False)
config.show()

# Restaurer configuration précédente
print("\n=== Restauration ===")
config.restore(history.get_backup(0))
config.show()


[OK] 18. OBSERVER - "Notifier les changements"

POURQUOI?
- Notifier automatiquement plusieurs objets d'un changement
- Découpler sujet et observateurs
- Implémenter des systèmes événementiels

QUAND?
- Un objet doit notifier d'autres objets
- Nombre variable d'observateurs
- Pattern pub/sub

COMMENT?

# === Exemple: Système de notifications ===

class Subject:
    def __init__(self):
        self._observers = []
        self._state = None
    
    def attach(self, observer):
        if observer not in self._observers:
            self._observers.append(observer)
            print(f"  Observer {observer.__class__.__name__} attaché")
    
    def detach(self, observer):
        if observer in self._observers:
            self._observers.remove(observer)
            print(f"  Observer {observer.__class__.__name__} détaché")
    
    def notify(self):
        for observer in self._observers:
            observer.update(self)
    
    def set_state(self, state):
        print(f"\n[Subject] Changement d'état: {state}")
        self._state = state
        self.notify()
    
    def get_state(self):
        return self._state

class Observer:
    def update(self, subject):
        raise NotImplementedError

class EmailObserver(Observer):
    def update(self, subject):
        state = subject.get_state()
        print(f"  [EMAIL] Email envoyé: État changé en '{state}'")

class SMSObserver(Observer):
    def update(self, subject):
        state = subject.get_state()
        print(f"  [MOBILE] SMS envoyé: Nouveau état: '{state}'")

class LogObserver(Observer):
    def update(self, subject):
        state = subject.get_state()
        import datetime
        timestamp = datetime.datetime.now().strftime("%H:%M:%S")
        print(f"  [NOTE] Log [{timestamp}]: État = '{state}'")

# Utilisation
subject = Subject()

email_obs = EmailObserver()
sms_obs = SMSObserver()
log_obs = LogObserver()

subject.attach(email_obs)
subject.attach(sms_obs)
subject.attach(log_obs)

subject.set_state("En cours")
subject.set_state("Terminé")

print("\n=== Détachement du SMS Observer ===")
subject.detach(sms_obs)
subject.set_state("Archivé")


# === Exemple: Données boursières ===

class Stock:
    def __init__(self, symbol, price):
        self.symbol = symbol
        self._price = price
        self._observers = []
    
    def attach(self, observer):
        self._observers.append(observer)
    
    def detach(self, observer):
        self._observers.remove(observer)
    
    def notify(self):
        for observer in self._observers:
            observer.update(self)
    
    @property
    def price(self):
        return self._price
    
    @price.setter
    def price(self, value):
        print(f"\n[{self.symbol}] Prix: ${self._price} -> ${value}")
        self._price = value
        self.notify()

class Investor(Observer):
    def __init__(self, name):
        self.name = name
    
    def update(self, stock):
        price = stock.price
        if price > 100:
            print(f"  {self.name}: Je vends {stock.symbol}!")
        elif price < 50:
            print(f"  {self.name}: J'achète {stock.symbol}!")
        else:
            print(f"  {self.name}: J'observe {stock.symbol}")

# Utilisation
apple = Stock("AAPL", 75)

investor1 = Investor("Alice")
investor2 = Investor("Bob")
investor3 = Investor("Charlie")

apple.attach(investor1)
apple.attach(investor2)
apple.attach(investor3)

apple.price = 45  # Tous achètent
apple.price = 80  # Tous observent
apple.price = 105  # Tous vendent


# === Exemple: Système d'événements de jeu ===

class GameEvent:
    def __init__(self):
        self._listeners = {}
    
    def subscribe(self, event_type, listener):
        if event_type not in self._listeners:
            self._listeners[event_type] = []
        self._listeners[event_type].append(listener)
        print(f"  Listener inscrit pour '{event_type}'")
    
    def unsubscribe(self, event_type, listener):
        if event_type in self._listeners:
            self._listeners[event_type].remove(listener)
    
    def notify(self, event_type, data=None):
        print(f"\n[Event] {event_type} déclenché")
        if event_type in self._listeners:
            for listener in self._listeners[event_type]:
                listener.on_event(event_type, data)

class EventListener:
    def on_event(self, event_type, data):
        raise NotImplementedError

class AchievementSystem(EventListener):
    def on_event(self, event_type, data):
        if event_type == "enemy_killed":
            print(f"  [TROPHEE] Achievement débloqué: Première victime!")
        elif event_type == "level_complete":
            print(f"  [TROPHEE] Achievement: Niveau {data} terminé!")

class SoundSystem(EventListener):
    def on_event(self, event_type, data):
        if event_type == "enemy_killed":
            print(f"  [SON] Son: *BOOM*")
        elif event_type == "level_complete":
            print(f"  [SON] Musique: Victory Theme!")

class StatisticsTracker(EventListener):
    def __init__(self):
        self.kills = 0
        self.levels_completed = 0
    
    def on_event(self, event_type, data):
        if event_type == "enemy_killed":
            self.kills += 1
            print(f"  [GRAPHIQUE] Stats: {self.kills} ennemis tués")
        elif event_type == "level_complete":
            self.levels_completed += 1
            print(f"  [GRAPHIQUE] Stats: {self.levels_completed} niveaux complétés")

# Utilisation
game_events = GameEvent()

achievements = AchievementSystem()
sounds = SoundSystem()
stats = StatisticsTracker()

game_events.subscribe("enemy_killed", achievements)
game_events.subscribe("enemy_killed", sounds)
game_events.subscribe("enemy_killed", stats)

game_events.subscribe("level_complete", achievements)
game_events.subscribe("level_complete", sounds)
game_events.subscribe("level_complete", stats)

# Déclencher des événements
game_events.notify("enemy_killed")
game_events.notify("enemy_killed")
game_events.notify("level_complete", 1)


[OK] 19. STATE - "Changer le comportement selon l'état"

POURQUOI?
- Changer le comportement d'un objet selon son état
- Éviter les if/else massifs
- Chaque état a sa propre logique

QUAND?
- Comportement dépend de l'état
- Beaucoup de conditions sur l'état
- Transitions d'état complexes

COMMENT?

# === Exemple: Distributeur automatique ===

from abc import ABC, abstractmethod

class State(ABC):
    @abstractmethod
    def insert_coin(self, machine):
        pass
    
    @abstractmethod
    def eject_coin(self, machine):
        pass
    
    @abstractmethod
    def select_product(self, machine):
        pass
    
    @abstractmethod
    def dispense(self, machine):
        pass

class NoCoinState(State):
    def insert_coin(self, machine):
        print("[ARGENT] Pièce insérée")
        machine.set_state(machine.has_coin_state)
    
    def eject_coin(self, machine):
        print("[X] Pas de pièce à éjecter")
    
    def select_product(self, machine):
        print("[X] Insérez une pièce d'abord")
    
    def dispense(self, machine):
        print("[X] Payez d'abord")

class HasCoinState(State):
    def insert_coin(self, machine):
        print("[X] Pièce déjà insérée")
    
    def eject_coin(self, machine):
        print("[ARGENT] Pièce éjectée")
        machine.set_state(machine.no_coin_state)
    
    def select_product(self, machine):
        print("[OK] Produit sélectionné")
        machine.set_state(machine.sold_state)
    
    def dispense(self, machine):
        print("[X] Sélectionnez un produit d'abord")

class SoldState(State):
    def insert_coin(self, machine):
        print("[HOURGLASS_WITH_FLOWING_SAND] Patientez...")
    
    def eject_coin(self, machine):
        print("[X] Trop tard!")
    
    def select_product(self, machine):
        print("[HOURGLASS_WITH_FLOWING_SAND] Distribution en cours...")
    
    def dispense(self, machine):
        print("[PACKAGE] Produit distribué!")
        machine.count -= 1
        if machine.count > 0:
            machine.set_state(machine.no_coin_state)
        else:
            print("[INTERDIT] Machine vide")
            machine.set_state(machine.sold_out_state)

class SoldOutState(State):
    def insert_coin(self, machine):
        print("[X] Machine vide")
    
    def eject_coin(self, machine):
        print("[X] Pas de pièce insérée")
    
    def select_product(self, machine):
        print("[X] Plus de produits")
    
    def dispense(self, machine):
        print("[X] Plus de produits")

class VendingMachine:
    def __init__(self, count):
        self.no_coin_state = NoCoinState()
        self.has_coin_state = HasCoinState()
        self.sold_state = SoldState()
        self.sold_out_state = SoldOutState()
        
        self.count = count
        if count > 0:
            self.state = self.no_coin_state
        else:
            self.state = self.sold_out_state
    
    def set_state(self, state):
        self.state = state
    
    def insert_coin(self):
        self.state.insert_coin(self)
    
    def eject_coin(self):
        self.state.eject_coin(self)
    
    def select_product(self):
        self.state.select_product(self)
        self.state.dispense(self)

# Utilisation
machine = VendingMachine(2)

print("=== Achat normal ===")
machine.insert_coin()
machine.select_product()

print("\n=== Changement d'avis ===")
machine.insert_coin()
machine.eject_coin()

print("\n=== Dernier produit ===")
machine.insert_coin()
machine.select_product()

print("\n=== Machine vide ===")
machine.insert_coin()


# === Exemple: Connexion TCP ===

class TCPState(ABC):
    @abstractmethod
    def open(self, connection):
        pass
    
    @abstractmethod
    def close(self, connection):
        pass
    
    @abstractmethod
    def send(self, connection, data):
        pass

class ClosedState(TCPState):
    def open(self, connection):
        print("[RESEAU] Ouverture connexion...")
        connection.set_state(connection.listen_state)
    
    def close(self, connection):
        print("[X] Déjà fermée")
    
    def send(self, connection, data):
        print("[X] Connexion fermée")

class ListenState(TCPState):
    def open(self, connection):
        print("[OK] Connexion établie")
        connection.set_state(connection.established_state)
    
    def close(self, connection):
        print("[VERROUILLE] Fermeture...")
        connection.set_state(connection.closed_state)
    
    def send(self, connection, data):
        print("[HOURGLASS_WITH_FLOWING_SAND] En attente de connexion...")

class EstablishedState(TCPState):
    def open(self, connection):
        print("[OK] Déjà ouverte")
    
    def close(self, connection):
        print("[VERROUILLE] Fermeture connexion...")
        connection.set_state(connection.closed_state)
    
    def send(self, connection, data):
        print(f"[SORTIE] Envoi: {data}")

class TCPConnection:
    def __init__(self):
        self.closed_state = ClosedState()
        self.listen_state = ListenState()
        self.established_state = EstablishedState()
        
        self.state = self.closed_state
    
    def set_state(self, state):
        self.state = state
    
    def open(self):
        self.state.open(self)
    
    def close(self):
        self.state.close(self)
    
    def send(self, data):
        self.state.send(self, data)

# Utilisation
connection = TCPConnection()

connection.send("Hello")  # [X] Connexion fermée
connection.open()         # Ouverture
connection.open()         # Établie
connection.send("Hello World")  # [OK] Envoi
connection.close()        # Fermeture
connection.send("Bye")    # [X] Connexion fermée


# === Exemple: Lecteur audio ===

class AudioPlayerState(ABC):
    @abstractmethod
    def play(self, player):
        pass
    
    @abstractmethod
    def pause(self, player):
        pass
    
    @abstractmethod
    def stop(self, player):
        pass

class StoppedState(AudioPlayerState):
    def play(self, player):
        print("[BLACK_RIGHT-POINTING_TRIANGLE]  Lecture de la piste")
        player.set_state(player.playing_state)
    
    def pause(self, player):
        print("[X] Déjà arrêté")
    
    def stop(self, player):
        print("[X] Déjà arrêté")

class PlayingState(AudioPlayerState):
    def play(self, player):
        print("[OK] Déjà en lecture")
    
    def pause(self, player):
        print("[DOUBLE_VERTICAL_BAR]  Pause")
        player.set_state(player.paused_state)
    
    def stop(self, player):
        print("[BLACK_SQUARE_FOR_STOP]  Arrêt")
        player.set_state(player.stopped_state)

class PausedState(AudioPlayerState):
    def play(self, player):
        print("[BLACK_RIGHT-POINTING_TRIANGLE]  Reprise de la lecture")
        player.set_state(player.playing_state)
    
    def pause(self, player):
        print("[X] Déjà en pause")
    
    def stop(self, player):
        print("[BLACK_SQUARE_FOR_STOP]  Arrêt")
        player.set_state(player.stopped_state)

class AudioPlayer:
    def __init__(self):
        self.stopped_state = StoppedState()
        self.playing_state = PlayingState()
        self.paused_state = PausedState()
        
        self.state = self.stopped_state
    
    def set_state(self, state):
        self.state = state
    
    def play(self):
        self.state.play(self)
    
    def pause(self):
        self.state.pause(self)
    
    def stop(self):
        self.state.stop(self)

# Utilisation
player = AudioPlayer()

player.play()   # Lecture
player.pause()  # Pause
player.play()   # Reprise
player.stop()   # Arrêt


[OK] 20. STRATEGY - "Famille d'algorithmes interchangeables"

POURQUOI?
- Définir une famille d'algorithmes
- Rendre les algorithmes interchangeables
- Éviter les if/else pour choisir l'algorithme

QUAND?
- Plusieurs façons de faire la même chose
- Besoin de changer d'algorithme à l'exécution
- Éviter code dupliqué

COMMENT?

# === Exemple: Méthodes de paiement ===

from abc import ABC, abstractmethod

class PaymentStrategy(ABC):
    @abstractmethod
    def pay(self, amount):
        pass

class CreditCardStrategy(PaymentStrategy):
    def __init__(self, card_number, cvv):
        self.card_number = card_number
        self.cvv = cvv
    
    def pay(self, amount):
        print(f"[CARTE] Paiement de ${amount} par carte **** {self.card_number[-4:]}")
        return True

class PayPalStrategy(PaymentStrategy):
    def __init__(self, email):
        self.email = email
    
    def pay(self, amount):
        print(f"🅿  Paiement de ${amount} via PayPal ({self.email})")
        return True

class BitcoinStrategy(PaymentStrategy):
    def __init__(self, wallet_address):
        self.wallet_address = wallet_address
    
    def pay(self, amount):
        btc = amount / 50000  # Conversion fictive
        print(f"₿  Paiement de {btc:.6f} BTC via {self.wallet_address[:10]}...")
        return True

class ShoppingCart:
    def __init__(self):
        self.items = []
        self.payment_strategy = None
    
    def add_item(self, item, price):
        self.items.append((item, price))
        print(f"Ajouté: {item} - ${price}")
    
    def set_payment_strategy(self, strategy):
        self.payment_strategy = strategy
    
    def checkout(self):
        total = sum(price for _, price in self.items)
        print(f"\n[ARGENT] Total: ${total}")
        
        if self.payment_strategy:
            return self.payment_strategy.pay(total)
        else:
            print("[X] Méthode de paiement non définie")
            return False

# Utilisation
cart = ShoppingCart()
cart.add_item("Laptop", 999)
cart.add_item("Mouse", 25)

print("\n=== Paiement par carte ===")
cart.set_payment_strategy(CreditCardStrategy("1234567890123456", "123"))
cart.checkout()

print("\n=== Paiement par PayPal ===")
cart.set_payment_strategy(PayPalStrategy("alice@example.com"))
cart.checkout()


# === Exemple: Tri de données ===

class SortStrategy(ABC):
    @abstractmethod
    def sort(self, data):
        pass

class BubbleSortStrategy(SortStrategy):
    def sort(self, data):
        print("[SYNC] Tri à bulles...")
        arr = data.copy()
        n = len(arr)
        for i in range(n):
            for j in range(0, n-i-1):
                if arr[j] > arr[j+1]:
                    arr[j], arr[j+1] = arr[j+1], arr[j]
        return arr

class QuickSortStrategy(SortStrategy):
    def sort(self, data):
        print("[RAPIDE] Tri rapide...")
        if len(data) <= 1:
            return data
        pivot = data[len(data) // 2]
        left = [x for x in data if x < pivot]
        middle = [x for x in data if x == pivot]
        right = [x for x in data if x > pivot]
        return self.sort(left) + middle + self.sort(right)

class MergeSortStrategy(SortStrategy):
    def sort(self, data):
        print("[MELANGE] Tri fusion...")
        if len(data) <= 1:
            return data
        
        mid = len(data) // 2
        left = self.sort(data[:mid])
        right = self.sort(data[mid:])
        
        return self._merge(left, right)
    
    def _merge(self, left, right):
        result = []
        i = j = 0
        
        while i < len(left) and j < len(right):
            if left[i] <= right[j]:
                result.append(left[i])
                i += 1
            else:
                result.append(right[j])
                j += 1
        
        result.extend(left[i:])
        result.extend(right[j:])
        return result

class DataSorter:
    def __init__(self, strategy):
        self.strategy = strategy
    
    def set_strategy(self, strategy):
        self.strategy = strategy
    
    def sort(self, data):
        return self.strategy.sort(data)

# Utilisation
data = [64, 34, 25, 12, 22, 11, 90]
print(f"Données: {data}\n")

sorter = DataSorter(BubbleSortStrategy())
result = sorter.sort(data)
print(f"Résultat: {result}\n")

sorter.set_strategy(QuickSortStrategy())
result = sorter.sort(data)
print(f"Résultat: {result}\n")


# === Exemple: Compression de fichiers ===

class CompressionStrategy(ABC):
    @abstractmethod
    def compress(self, data):
        pass

class ZipCompression(CompressionStrategy):
    def compress(self, data):
        print(f"[PACKAGE] Compression ZIP de '{data}'")
        return f"{data}.zip"

class RarCompression(CompressionStrategy):
    def compress(self, data):
        print(f"[PACKAGE] Compression RAR de '{data}'")
        return f"{data}.rar"

class TarGzCompression(CompressionStrategy):
    def compress(self, data):
        print(f"[PACKAGE] Compression TAR.GZ de '{data}'")
        return f"{data}.tar.gz"

class FileCompressor:
    def __init__(self, strategy):
        self.strategy = strategy
    
    def compress_file(self, filename):
        return self.strategy.compress(filename)

# Utilisation
compressor = FileCompressor(ZipCompression())
result = compressor.compress_file("document.txt")

compressor.strategy = RarCompression()
result = compressor.compress_file("photo.jpg")


# === Exemple: Navigation (GPS) ===

class RouteStrategy(ABC):
    @abstractmethod
    def calculate_route(self, start, end):
        pass

class CarRouteStrategy(RouteStrategy):
    def calculate_route(self, start, end):
        print(f"[VOITURE] Itinéraire voiture de {start} à {end}")
        print("  -> Autoroute A1")
        print("  -> Durée: 45 min")
        return ["A1", "sortie 12"]

class WalkingRouteStrategy(RouteStrategy):
    def calculate_route(self, start, end):
        print(f"[PEDESTRIAN] Itinéraire à pied de {start} à {end}")
        print("  -> Rue piétonne")
        print("  -> Durée: 20 min")
        return ["Main St", "Park Ave"]

class BicycleRouteStrategy(RouteStrategy):
    def calculate_route(self, start, end):
        print(f"[BICYCLIST] Itinéraire vélo de {start} à {end}")
        print("  -> Piste cyclable")
        print("  -> Durée: 15 min")
        return ["Bike Lane 1", "Bridge"]

class Navigator:
    def __init__(self):
        self.strategy = None
    
    def set_strategy(self, strategy):
        self.strategy = strategy
    
    def navigate(self, start, end):
        if self.strategy:
            return self.strategy.calculate_route(start, end)
        print("[X] Mode de transport non défini")

# Utilisation
nav = Navigator()

print("=== En voiture ===")
nav.set_strategy(CarRouteStrategy())
nav.navigate("Paris", "Lyon")

print("\n=== À pied ===")
nav.set_strategy(WalkingRouteStrategy())
nav.navigate("Maison", "Bureau")


[OK] 21. TEMPLATE METHOD - "Squelette d'algorithme"

POURQUOI?
- Définir le squelette d'un algorithme
- Sous-classes redéfinissent certaines étapes
- Code réutilisable avec variation

QUAND?
- Algorithme avec étapes communes
- Variations dans certaines étapes seulement
- Éviter duplication de code

COMMENT?

# === Exemple: Préparation de boissons ===

from abc import ABC, abstractmethod

class BeverageTemplate(ABC):
    # Template method - définit le squelette
    def prepare(self):
        self.boil_water()
        self.brew()
        self.pour_in_cup()
        self.add_condiments()
        print("[OK] Boisson prête!\n")
    
    def boil_water(self):
        print("[DROPLET] Faire bouillir l'eau")
    
    def pour_in_cup(self):
        print("[HOT_BEVERAGE] Verser dans la tasse")
    
    # Méthodes abstraites - à implémenter
    @abstractmethod
    def brew(self):
        pass
    
    @abstractmethod
    def add_condiments(self):
        pass

class Tea(BeverageTemplate):
    def brew(self):
        print("[TEACUP_WITHOUT_HANDLE] Infuser le thé")
    
    def add_condiments(self):
        print("[LEMON] Ajouter du citron")

class Coffee(BeverageTemplate):
    def brew(self):
        print("[HOT_BEVERAGE] Faire passer le café")
    
    def add_condiments(self):
        print("[GLASS_OF_MILK] Ajouter lait et sucre")

# Utilisation
print("=== Préparation du thé ===")
tea = Tea()
tea.prepare()

print("=== Préparation du café ===")
coffee = Coffee()
coffee.prepare()


# === Exemple: Construction de maison ===

class HouseTemplate(ABC):
    # Template method
    def build_house(self):
        self.build_foundation()
        self.build_walls()
        self.build_roof()
        self.build_interior()
        print("[ACCUEIL] Maison construite!\n")
    
    def build_foundation(self):
        print("[CONSTRUCTION]  Fondations en béton")
    
    @abstractmethod
    def build_walls(self):
        pass
    
    @abstractmethod
    def build_roof(self):
        pass
    
    def build_interior(self):
        print("[DESIGN] Intérieur standard")

class WoodenHouse(HouseTemplate):
    def build_walls(self):
        print("[WOOD] Murs en bois")
    
    def build_roof(self):
        print("[WOOD] Toit en bois")

class ConcreteHouse(HouseTemplate):
    def build_walls(self):
        print("[BRICK] Murs en béton")
    
    def build_roof(self):
        print("[BRICK] Toit en béton armé")
    
    def build_interior(self):
        print("[DESIGN] Intérieur moderne")  # Override

# Utilisation
print("=== Maison en bois ===")
wooden = WoodenHouse()
wooden.build_house()

print("=== Maison en béton ===")
concrete = ConcreteHouse()
concrete.build_house()


# === Exemple avec hooks (méthodes optionnelles) ===

class DataProcessor(ABC):
    def process(self):
        self.read_data()
        self.process_data()
        
        if self.should_save():  # Hook
            self.save_data()
        
        if self.should_send_notification():  # Hook
            self.send_notification()
    
    @abstractmethod
    def read_data(self):
        pass
    
    @abstractmethod
    def process_data(self):
        pass
    
    @abstractmethod
    def save_data(self):
        pass
    
    # Hooks - méthodes optionnelles
    def should_save(self):
        return True
    
    def should_send_notification(self):
        return False
    
    def send_notification(self):
        print("[EMAIL] Notification envoyée")

class CSVProcessor(DataProcessor):
    def read_data(self):
        print("[FICHIER] Lecture CSV")
    
    def process_data(self):
        print("[CONFIG]  Traitement des données CSV")
    
    def save_data(self):
        print("[SAUVEGARDE] Sauvegarde CSV")

class JSONProcessor(DataProcessor):
    def read_data(self):
        print("[FICHIER] Lecture JSON")
    
    def process_data(self):
        print("[CONFIG]  Traitement des données JSON")
    
    def save_data(self):
        print("[SAUVEGARDE] Sauvegarde JSON")
    
    def should_send_notification(self):
        return True  # Override hook

# Utilisation
print("=== Traitement CSV ===")
csv = CSVProcessor()
csv.process()

print("\n=== Traitement JSON ===")
json_proc = JSONProcessor()
json_proc.process()


# === Exemple: Tests unitaires ===

class TestCase(ABC):
    def run(self):
        self.setup()
        try:
            self.test()
            print("[OK] Test réussi")
        except AssertionError as e:
            print(f"[X] Test échoué: {e}")
        finally:
            self.teardown()
    
    def setup(self):
        print("[OUTIL] Setup...")
    
    def teardown(self):
        print("[NETTOYAGE] Cleanup...")
    
    @abstractmethod
    def test(self):
        pass

class LoginTest(TestCase):
    def setup(self):
        super().setup()
        print("  -> Initialisation DB test")
    
    def test(self):
        print("[TEST] Test: Login utilisateur")
        # Simulation
        username = "test"
        password = "pass"
        assert username == "test", "Username invalide"
        assert password == "pass", "Password invalide"
    
    def teardown(self):
        print("  -> Nettoyage DB test")
        super().teardown()

class CalculatorTest(TestCase):
    def test(self):
        print("[TEST] Test: Addition")
        result = 2 + 2
        assert result == 4, f"Attendu 4, obtenu {result}"

# Utilisation
print("=== Test Login ===")
login_test = LoginTest()
login_test.run()

print("\n=== Test Calculator ===")
calc_test = CalculatorTest()
calc_test.run()


[OK] 22. VISITOR - "Ajouter des opérations sans modifier les classes"

POURQUOI?
- Ajouter de nouvelles opérations à des objets existants
- Séparer algorithme de structure d'objets
- Éviter pollution des classes avec nouvelles méthodes

QUAND?
- Structure d'objets stable
- Besoin d'ajouter souvent de nouvelles opérations
- Opérations sur objets de types différents

COMMENT?

# === Exemple: Calcul sur formes géométriques ===

from abc import ABC, abstractmethod

class ShapeVisitor(ABC):
    @abstractmethod
    def visit_circle(self, circle):
        pass
    
    @abstractmethod
    def visit_rectangle(self, rectangle):
        pass
    
    @abstractmethod
    def visit_triangle(self, triangle):
        pass

class Shape(ABC):
    @abstractmethod
    def accept(self, visitor):
        pass

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius
    
    def accept(self, visitor):
        return visitor.visit_circle(self)

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def accept(self, visitor):
        return visitor.visit_rectangle(self)

class Triangle(Shape):
    def __init__(self, base, height):
        self.base = base
        self.height = height
    
    def accept(self, visitor):
        return visitor.visit_triangle(self)

# Visitor pour calculer l'aire
class AreaCalculator(ShapeVisitor):
    def visit_circle(self, circle):
        import math
        area = math.pi * circle.radius ** 2
        return f"Aire du cercle: {area:.2f}"
    
    def visit_rectangle(self, rectangle):
        area = rectangle.width * rectangle.height
        return f"Aire du rectangle: {area:.2f}"
    
    def visit_triangle(self, triangle):
        area = (triangle.base * triangle.height) / 2
        return f"Aire du triangle: {area:.2f}"

# Visitor pour calculer le périmètre
class PerimeterCalculator(ShapeVisitor):
    def visit_circle(self, circle):
        import math
        perimeter = 2 * math.pi * circle.radius
        return f"Périmètre du cercle: {perimeter:.2f}"
    
    def visit_rectangle(self, rectangle):
        perimeter = 2 * (rectangle.width + rectangle.height)
        return f"Périmètre du rectangle: {perimeter:.2f}"
    
    def visit_triangle(self, triangle):
        # Simplifié - triangle équilatéral
        import math
        side = math.sqrt((triangle.base/2)**2 + triangle.height**2)
        perimeter = triangle.base + 2 * side
        return f"Périmètre du triangle: {perimeter:.2f}"

# Visitor pour exporter en JSON
class JSONExporter(ShapeVisitor):
    def visit_circle(self, circle):
        return f'{{"type": "circle", "radius": {circle.radius}}}'
    
    def visit_rectangle(self, rectangle):
        return f'{{"type": "rectangle", "width": {rectangle.width}, "height": {rectangle.height}}}'
    
    def visit_triangle(self, triangle):
        return f'{{"type": "triangle", "base": {triangle.base}, "height": {triangle.height}}}'

# Utilisation
shapes = [
    Circle(5),
    Rectangle(4, 6),
    Triangle(3, 4)
]

area_calc = AreaCalculator()
perimeter_calc = PerimeterCalculator()
json_export = JSONExporter()

print("=== Calcul d'aires ===")
for shape in shapes:
    print(shape.accept(area_calc))

print("\n=== Calcul de périmètres ===")
for shape in shapes:
    print(shape.accept(perimeter_calc))

print("\n=== Export JSON ===")
for shape in shapes:
    print(shape.accept(json_export))


# === Exemple: Système de fichiers ===

class FileSystemVisitor(ABC):
    @abstractmethod
    def visit_file(self, file):
        pass
    
    @abstractmethod
    def visit_directory(self, directory):
        pass

    class FileSystemElement(ABC):
    @abstractmethod
    def accept(self, visitor):
        pass

class File(FileSystemElement):
    def __init__(self, name, size):
        self.name = name
        self.size = size
    
    def accept(self, visitor):
        return visitor.visit_file(self)

class Directory(FileSystemElement):
    def __init__(self, name):
        self.name = name
        self.children = []
    
    def add(self, element):
        self.children.append(element)
    
    def accept(self, visitor):
        return visitor.visit_directory(self)

# Visitor pour calculer la taille totale
class SizeCalculatorVisitor(FileSystemVisitor):
    def visit_file(self, file):
        return file.size
    
    def visit_directory(self, directory):
        total = 0
        for child in directory.children:
            total += child.accept(self)
        return total

# Visitor pour afficher l'arborescence
class DisplayVisitor(FileSystemVisitor):
    def __init__(self):
        self.indent = 0
    
    def visit_file(self, file):
        print("  " * self.indent + f"[FICHIER] {file.name} ({file.size} KB)")
    
    def visit_directory(self, directory):
        print("  " * self.indent + f"[DOSSIER] {directory.name}/")
        self.indent += 1
        for child in directory.children:
            child.accept(self)
        self.indent -= 1

# Visitor pour rechercher
class SearchVisitor(FileSystemVisitor):
    def __init__(self, search_term):
        self.search_term = search_term
        self.results = []
    
    def visit_file(self, file):
        if self.search_term.lower() in file.name.lower():
            self.results.append(file.name)
    
    def visit_directory(self, directory):
        for child in directory.children:
            child.accept(self)

# Utilisation
root = Directory("root")
documents = Directory("documents")
photos = Directory("photos")

documents.add(File("report.pdf", 1024))
documents.add(File("notes.txt", 64))
photos.add(File("vacation.jpg", 2048))
photos.add(File("family.jpg", 1536))

root.add(documents)
root.add(photos)
root.add(File("readme.txt", 32))

print("=== Affichage ===")
display = DisplayVisitor()
root.accept(display)

print("\n=== Taille totale ===")
size_calc = SizeCalculatorVisitor()
total_size = root.accept(size_calc)
print(f"Taille totale: {total_size} KB")

print("\n=== Recherche 'jpg' ===")
search = SearchVisitor("jpg")
root.accept(search)
print("Fichiers trouvés:", search.results)


# === Exemple: Document HTML ===

class HTMLVisitor(ABC):
    @abstractmethod
    def visit_paragraph(self, paragraph):
        pass
    
    @abstractmethod
    def visit_heading(self, heading):
        pass
    
    @abstractmethod
    def visit_link(self, link):
        pass

class HTMLElement(ABC):
    @abstractmethod
    def accept(self, visitor):
        pass

class Paragraph(HTMLElement):
    def __init__(self, text):
        self.text = text
    
    def accept(self, visitor):
        visitor.visit_paragraph(self)

class Heading(HTMLElement):
    def __init__(self, level, text):
        self.level = level
        self.text = text
    
    def accept(self, visitor):
        visitor.visit_heading(self)

class Link(HTMLElement):
    def __init__(self, url, text):
        self.url = url
        self.text = text
    
    def accept(self, visitor):
        visitor.visit_link(self)

# Visitor pour générer HTML
class HTMLRenderer(HTMLVisitor):
    def __init__(self):
        self.html = []
    
    def visit_paragraph(self, paragraph):
        self.html.append(f"<p>{paragraph.text}</p>")
    
    def visit_heading(self, heading):
        self.html.append(f"<h{heading.level}>{heading.text}</h{heading.level}>")
    
    def visit_link(self, link):
        self.html.append(f'<a href="{link.url}">{link.text}</a>')
    
    def get_html(self):
        return "\n".join(self.html)

# Visitor pour extraire le texte
class PlainTextExtractor(HTMLVisitor):
    def __init__(self):
        self.text = []
    
    def visit_paragraph(self, paragraph):
        self.text.append(paragraph.text)
    
    def visit_heading(self, heading):
        self.text.append(paragraph.text.upper())
    
    def visit_link(self, link):
        self.text.append(f"{link.text} ({link.url})")
    
    def get_text(self):
        return "\n".join(self.text)

# Visitor pour compter les mots
class WordCounter(HTMLVisitor):
    def __init__(self):
        self.count = 0
    
    def visit_paragraph(self, paragraph):
        self.count += len(paragraph.text.split())
    
    def visit_heading(self, heading):
        self.count += len(heading.text.split())
    
    def visit_link(self, link):
        self.count += len(link.text.split())

# Utilisation
elements = [
    Heading(1, "Mon Blog"),
    Paragraph("Bienvenue sur mon blog."),
    Link("https://example.com", "Visitez mon site"),
    Paragraph("Merci de votre visite!")
]

print("=== Rendu HTML ===")
renderer = HTMLRenderer()
for elem in elements:
    elem.accept(renderer)
print(renderer.get_html())

print("\n=== Comptage de mots ===")
counter = WordCounter()
for elem in elements:
    elem.accept(counter)
print(f"Total: {counter.count} mots")


[OK] 23. INTERPRETER - "Interpréter un langage"

POURQUOI?
- Définir une grammaire pour un langage
- Interpréter des expressions
- Créer un DSL (Domain Specific Language)

QUAND?
- Grammaire simple
- Efficacité pas critique
- Expressions à interpréter fréquemment

COMMENT?

# === Exemple: Calculatrice avec expressions ===

from abc import ABC, abstractmethod

class Expression(ABC):
    @abstractmethod
    def interpret(self):
        pass

class Number(Expression):
    def __init__(self, value):
        self.value = value
    
    def interpret(self):
        return self.value

class Add(Expression):
    def __init__(self, left, right):
        self.left = left
        self.right = right
    
    def interpret(self):
        return self.left.interpret() + self.right.interpret()

class Subtract(Expression):
    def __init__(self, left, right):
        self.left = left
        self.right = right
    
    def interpret(self):
        return self.left.interpret() - self.right.interpret()

class Multiply(Expression):
    def __init__(self, left, right):
        self.left = left
        self.right = right
    
    def interpret(self):
        return self.left.interpret() * self.right.interpret()

class Divide(Expression):
    def __init__(self, left, right):
        self.left = left
        self.right = right
    
    def interpret(self):
        right_value = self.right.interpret()
        if right_value == 0:
            raise ValueError("Division par zéro")
        return self.left.interpret() / right_value

# Utilisation
# Expression: (5 + 3) * (10 - 2)
expr = Multiply(
    Add(Number(5), Number(3)),
    Subtract(Number(10), Number(2))
)

result = expr.interpret()
print(f"(5 + 3) * (10 - 2) = {result}")  # 64

# Expression: 20 / (5 - 3)
expr2 = Divide(
    Number(20),
    Subtract(Number(5), Number(3))
)

print(f"20 / (5 - 3) = {expr2.interpret()}")  # 10.0


# === Exemple: Expressions booléennes ===

class BooleanExpression(ABC):
    @abstractmethod
    def interpret(self, context):
        pass

class Constant(BooleanExpression):
    def __init__(self, value):
        self.value = value
    
    def interpret(self, context):
        return self.value

class Variable(BooleanExpression):
    def __init__(self, name):
        self.name = name
    
    def interpret(self, context):
        return context.get(self.name, False)

class AndExpression(BooleanExpression):
    def __init__(self, left, right):
        self.left = left
        self.right = right
    
    def interpret(self, context):
        return self.left.interpret(context) and self.right.interpret(context)

class OrExpression(BooleanExpression):
    def __init__(self, left, right):
        self.left = left
        self.right = right
    
    def interpret(self, context):
        return self.left.interpret(context) or self.right.interpret(context)

class NotExpression(BooleanExpression):
    def __init__(self, expr):
        self.expr = expr
    
    def interpret(self, context):
        return not self.expr.interpret(context)

# Utilisation
# Expression: (A AND B) OR (NOT C)
context = {
    'A': True,
    'B': False,
    'C': True
}

expr = OrExpression(
    AndExpression(Variable('A'), Variable('B')),
    NotExpression(Variable('C'))
)

result = expr.interpret(context)
print(f"(A AND B) OR (NOT C) = {result}")  # False

# Changer contexte
context['B'] = True
result = expr.interpret(context)
print(f"Avec B=True: {result}")  # True


# === Exemple: Langage de commandes ===

class Command(ABC):
    @abstractmethod
    def execute(self, robot):
        pass

class Robot:
    def __init__(self):
        self.x = 0
        self.y = 0
        self.direction = 'N'  # N, S, E, W
    
    def move_forward(self):
        if self.direction == 'N':
            self.y += 1
        elif self.direction == 'S':
            self.y -= 1
        elif self.direction == 'E':
            self.x += 1
        elif self.direction == 'W':
            self.x -= 1
        print(f"  -> Position: ({self.x}, {self.y})")
    
    def turn_left(self):
        turns = {'N': 'W', 'W': 'S', 'S': 'E', 'E': 'N'}
        self.direction = turns[self.direction]
        print(f"  -> Direction: {self.direction}")
    
    def turn_right(self):
        turns = {'N': 'E', 'E': 'S', 'S': 'W', 'W': 'N'}
        self.direction = turns[self.direction]
        print(f"  -> Direction: {self.direction}")
    
    def show_position(self):
        print(f"Position finale: ({self.x}, {self.y}), Direction: {self.direction}")

class MoveCommand(Command):
    def execute(self, robot):
        print("Avancer")
        robot.move_forward()

class LeftCommand(Command):
    def execute(self, robot):
        print("Tourner à gauche")
        robot.turn_left()

class RightCommand(Command):
    def execute(self, robot):
        print("Tourner à droite")
        robot.turn_right()

class RepeatCommand(Command):
    def __init__(self, times, commands):
        self.times = times
        self.commands = commands
    
    def execute(self, robot):
        print(f"Répéter {self.times} fois:")
        for _ in range(self.times):
            for cmd in self.commands:
                cmd.execute(robot)

# Parser simple
def parse_commands(text):
    commands = []
    for char in text.upper():
        if char == 'M':
            commands.append(MoveCommand())
        elif char == 'L':
            commands.append(LeftCommand())
        elif char == 'R':
            commands.append(RightCommand())
    return commands

# Utilisation
robot = Robot()

# Programme: Avancer, tourner à droite, avancer
program = [
    MoveCommand(),
    RightCommand(),
    MoveCommand(),
    MoveCommand()
]

print("=== Exécution du programme ===")
for cmd in program:
    cmd.execute(robot)

robot.show_position()

# Programme avec répétition: dessiner un carré
print("\n=== Dessiner un carré ===")
robot2 = Robot()
square_side = [MoveCommand(), RightCommand()]
square_program = RepeatCommand(4, square_side)
square_program.execute(robot2)
robot2.show_position()


# === Exemple: Requêtes SQL simplifiées ===

class SQLExpression(ABC):
    @abstractmethod
    def interpret(self, database):
        pass

class SelectExpression(SQLExpression):
    def __init__(self, table, condition=None):
        self.table = table
        self.condition = condition
    
    def interpret(self, database):
        if self.table not in database:
            return []
        
        rows = database[self.table]
        
        if self.condition:
            return [row for row in rows if self.condition.evaluate(row)]
        
        return rows

class Condition(ABC):
    @abstractmethod
    def evaluate(self, row):
        pass

class EqualsCondition(Condition):
    def __init__(self, field, value):
        self.field = field
        self.value = value
    
    def evaluate(self, row):
        return row.get(self.field) == self.value

class GreaterThanCondition(Condition):
    def __init__(self, field, value):
        self.field = field
        self.value = value
    
    def evaluate(self, row):
        return row.get(self.field, 0) > self.value

# Base de données simulée
database = {
    'users': [
        {'id': 1, 'name': 'Alice', 'age': 30},
        {'id': 2, 'name': 'Bob', 'age': 25},
        {'id': 3, 'name': 'Charlie', 'age': 35},
    ]
}

# SELECT * FROM users
query1 = SelectExpression('users')
result = query1.interpret(database)
print("SELECT * FROM users:")
for row in result:
    print(f"  {row}")

# SELECT * FROM users WHERE age > 28
query2 = SelectExpression('users', GreaterThanCondition('age', 28))
result = query2.interpret(database)
print("\nSELECT * FROM users WHERE age > 28:")
for row in result:
    print(f"  {row}")


╔════════════════════════════════════════════════════════════════════╗
║                    RÉSUMÉ & COMPARAISON                            ║
╚════════════════════════════════════════════════════════════════════╝

[OK] QUAND UTILISER QUEL PATTERN?


PROBLÈME                              -> PATTERN À UTILISER
═══════════════════════════════════════════════════════════════════

# Création d'objets
Une seule instance                   -> SINGLETON
Création complexe                    -> BUILDER
Familles d'objets                    -> ABSTRACT FACTORY
Déléguer la création                 -> FACTORY METHOD
Cloner objets                        -> PROTOTYPE

# Structure
Adapter interface                    -> ADAPTER
Séparer abstraction/implémentation   -> BRIDGE
Arbre d'objets                       -> COMPOSITE
Ajouter fonctionnalités              -> DECORATOR
Interface simplifiée                 -> FACADE
Économiser mémoire                   -> FLYWEIGHT
Contrôler accès                      -> PROXY

# Comportement
Chaîne de traitement                 -> CHAIN OF RESPONSIBILITY
Encapsuler requête                   -> COMMAND
Parcourir collection                 -> ITERATOR
Centraliser communication            -> MEDIATOR
Sauvegarder état                     -> MEMENTO
Notifier changements                 -> OBSERVER
Comportement selon état              -> STATE
Algorithmes interchangeables         -> STRATEGY
Squelette d'algorithme               -> TEMPLATE METHOD
Ajouter opérations                   -> VISITOR
Interpréter langage                  -> INTERPRETER


[OK] PATTERNS SOUVENT CONFONDUS


STRATEGY vs STATE
─────────────────
STRATEGY: Choisir l'algorithme (client décide)
STATE: État change le comportement (objet décide)

Exemple STRATEGY: Choisir mode de paiement
Exemple STATE: Distributeur change état automatiquement


DECORATOR vs PROXY
──────────────────
DECORATOR: Ajouter fonctionnalités (même interface)
PROXY: Contrôler accès (peut avoir interface différente)

Exemple DECORATOR: Café + lait + sucre
Exemple PROXY: Protection, lazy loading, cache


FACTORY METHOD vs ABSTRACT FACTORY
───────────────────────────────────
FACTORY METHOD: Créer UN type d'objet
ABSTRACT FACTORY: Créer une FAMILLE d'objets

Exemple FACTORY: Créer Document (PDF/Word/Excel)
Exemple ABSTRACT FACTORY: Créer UI complète (Windows/Mac/Linux)


ADAPTER vs BRIDGE
─────────────────
ADAPTER: Adapter code existant incompatible
BRIDGE: Concevoir pour séparer abstraction/implémentation

Exemple ADAPTER: Adapter API PayPal existante
Exemple BRIDGE: Concevoir formes avec différents rendus


COMPOSITE vs DECORATOR
──────────────────────
COMPOSITE: Structure arborescente (parent-enfant)
DECORATOR: Emballage successif (même interface)

Exemple COMPOSITE: Dossiers et fichiers
Exemple DECORATOR: Notifications multi-canaux


COMMAND vs STRATEGY
───────────────────
COMMAND: Encapsuler action (peut undo/redo)
STRATEGY: Encapsuler algorithme (pas d'historique)

Exemple COMMAND: Éditeur avec undo
Exemple STRATEGY: Tri avec différents algos


OBSERVER vs MEDIATOR
────────────────────
OBSERVER: Un-à-plusieurs (broadcast)
MEDIATOR: Plusieurs-à-plusieurs (centralisé)

Exemple OBSERVER: Sujet notifie observateurs
Exemple MEDIATOR: Chat room coordonne users


[OK] COMBINAISONS COURANTES


SINGLETON + FACTORY
───────────────────
Factory unique dans toute l'application

class DatabaseFactory:
    _instance = None
    
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance


COMPOSITE + VISITOR
───────────────────
Opérations sur structures arborescentes

# Visitor parcourt Composite pour calculer/afficher


DECORATOR + FACTORY
───────────────────
Factory crée objets décorés

def create_secure_logger():
    logger = BasicLogger()
    logger = EncryptionDecorator(logger)
    logger = CompressionDecorator(logger)
    return logger


STRATEGY + TEMPLATE METHOD
───────────────────────────
Template définit squelette, Strategy pour étapes variables

class DataImporter(ABC):
    def import_data(self):
        self.read()
        self.validate()
        self.transform()  # Strategy ici!
        self.save()


COMMAND + MEMENTO
─────────────────
Command sauvegarde état pour undo

class EditCommand(Command):
    def execute(self):
        self.memento = self.editor.save()
        # ... modifications
    
    def undo(self):
        self.editor.restore(self.memento)


[OK] PRINCIPES SOLID ET PATTERNS


Single Responsibility
─────────────────────
Chaque classe une seule responsabilité
-> FACADE, PROXY, DECORATOR

Open/Closed
───────────
Ouvert extension, fermé modification
-> STRATEGY, DECORATOR, VISITOR

Liskov Substitution
───────────────────
Sous-classes remplaçables
-> FACTORY METHOD, ABSTRACT FACTORY

Interface Segregation
─────────────────────
Interfaces petites et spécifiques
-> ADAPTER, BRIDGE

Dependency Inversion
────────────────────
Dépendre des abstractions
-> FACTORY, STRATEGY, OBSERVER


[OK] ANTI-PATTERNS À ÉVITER


[X] God Object
Objet qui fait tout
Solution: FACADE, MEDIATOR pour séparer responsabilités

[X] Spaghetti Code
Code entremêlé
Solution: STRATEGY, STATE pour organiser

[X] Lava Flow
Code mort non supprimé
Solution: Refactoring avec patterns appropriés

[X] Golden Hammer
Utiliser même pattern partout
Solution: Choisir le bon pattern selon contexte

[X] Over-Engineering
Trop de patterns inutiles
Solution: YAGNI (You Aren't Gonna Need It)


[OK] CONSEILS PRATIQUES


1. Commencer simple
──────────────────
Ne pas ajouter de pattern tant que pas nécessaire
Refactorer vers pattern quand besoin apparaît


2. Patterns les plus utiles au quotidien
────────────────────────────────────────
- SINGLETON (config, logger)
- FACTORY (création objets)
- DECORATOR (ajouter fonctionnalités)
- OBSERVER (événements)
- STRATEGY (algorithmes interchangeables)


3. Apprendre progressivement
────────────────────────────
Niveau 1: SINGLETON, FACTORY, DECORATOR
Niveau 2: OBSERVER, STRATEGY, ADAPTER
Niveau 3: Tous les autres


4. Reconnaître dans le code existant
────────────────────────────────────
- Django: TEMPLATE METHOD (views)
- Flask: DECORATOR (@app.route)
- Python: ITERATOR (for loops)
- Logging: SINGLETON


5. Documenter l'utilisation
───────────────────────────
Toujours commenter quel pattern est utilisé et pourquoi


6. Tester les patterns
──────────────────────
Patterns bien implémentés facilitent les tests unitaires


[OK] RESSOURCES


Livres:
──────
- "Design Patterns" (Gang of Four) - Livre original
- "Head First Design Patterns" - Approche visuelle
- "Python Design Patterns" - Spécifique Python

Sites:
──────
- refactoring.guru - Explications visuelles excellentes
- sourcemaking.com - Diagrammes et exemples
- python-patterns.guide - Patterns en Python

Pratique:
─────────
- Refactorer du code existant vers patterns
- Identifier patterns dans bibliothèques connues
- Créer petits projets pour pratiquer chaque pattern


[OK] EXERCICES PRATIQUES


Exercice 1: E-commerce
──────────────────────
Créer système e-commerce utilisant:
- SINGLETON: Panier d'achat
- FACTORY: Créer produits
- STRATEGY: Méthodes de paiement
- OBSERVER: Notifications commande
- DECORATOR: Options produit


Exercice 2: Éditeur de texte
────────────────────────────
- COMMAND: Undo/Redo
- MEMENTO: Sauvegarder état
- OBSERVER: Mettre à jour UI
- DECORATOR: Formatage texte


Exercice 3: Jeu vidéo
─────────────────────
- STATE: États du joueur
- FACTORY: Créer ennemis
- OBSERVER: Système événements
- COMPOSITE: Inventaire items
- STRATEGY: IA ennemis


[OK] CHECKLIST AVANT D'UTILISER UN PATTERN


[WHITE_SQUARE] Le problème est-il bien compris?
[WHITE_SQUARE] Le pattern résout-il vraiment le problème?
[WHITE_SQUARE] La solution simple ne suffit-elle pas?
[WHITE_SQUARE] L'équipe comprend-elle le pattern?
[WHITE_SQUARE] Le pattern facilite-t-il la maintenance?
[WHITE_SQUARE] Y a-t-il des alternatives plus simples?


═══════════════════════════════════════════════════════════════════
                            FIN
═══════════════════════════════════════════════════════════════════

Ce fichier couvre les 23 Design Patterns du Gang of Four avec:
[OK] Explication POURQUOI les utiliser
[OK] QUAND les utiliser
[OK] COMMENT les implémenter en Python
[OK] Exemples pratiques multiples
[OK] Comparaisons et différences
[OK] Conseils et bonnes pratiques

N'oubliez pas: Les patterns sont des outils, pas des obligations!
Utilisez-les quand ils apportent de la valeur, pas par principe.

Bon coding! [RAPIDE]# Fichier: python_cheats/cheatsheets/design_pattern.txt
# Cheatsheet Design Patterns Python - Guide Complet pour Débutants
# Les 23 Patterns du Gang of Four (GoF)