Menu
Coddy logo textTech

Patron Décorateur

Fait partie de la section Object Oriented Programming du Journey Python de Coddy — leçon 51 sur 64.

Le patron Décorateur ajoute dynamiquement de nouvelles fonctionnalités aux objets sans modifier leur structure. Il enveloppe les objets pour étendre leur comportement.

Voici un exemple simple de café :

class Coffee:
    def cost(self):
        return 5
    
    def description(self):
        return "Simple coffee"

Créez des décorateurs qui ajoutent des fonctionnalités au café :

class MilkDecorator:
    def __init__(self, coffee):
        self.coffee = coffee
    
    def cost(self):
        return self.coffee.cost() + 2
    
    def description(self):
        return self.coffee.description() + " + Milk"

class SugarDecorator:
    def __init__(self, coffee):
        self.coffee = coffee
    
    def cost(self):
        return self.coffee.cost() + 1
    
    def description(self):
        return self.coffee.description() + " + Sugar"

Chaque décorateur enveloppe un autre objet et ajoute sa propre fonctionnalité.

Utilisez des décorateurs pour créer un café personnalisé :

# Commencer avec un café de base
my_coffee = Coffee()
print(f"{my_coffee.description()}: ${my_coffee.cost()}")

# Ajouter du lait
my_coffee = MilkDecorator(my_coffee)
print(f"{my_coffee.description()}: ${my_coffee.cost()}")

# Ajouter du sucre
my_coffee = SugarDecorator(my_coffee)
print(f"{my_coffee.description()}: ${my_coffee.cost()}")

Créez un autre exemple avec du formatage de texte :

class Text:
    def __init__(self, content):
        self.content = content
    
    def render(self):
        return self.content

class BoldDecorator:
    def __init__(self, text):
        self.text = text
    
    def render(self):
        return f"<b>{self.text.render()}</b>"

class ItalicDecorator:
    def __init__(self, text):
        self.text = text
    
    def render(self):
        return f"<i>{self.text.render()}</i>"

# Construire le texte formaté étape par étape
message = Text("Hello World")
message = BoldDecorator(message)
message = ItalicDecorator(message)

print(message.render())

Sortie :

Simple coffee: $5
Simple coffee + Milk: $7
Simple coffee + Milk + Sugar: $8
<i><b>Hello World</b></i>

Point clé : Le motif Décorateur (Decorator Pattern) enveloppe des objets pour ajouter de nouveaux comportements sans modifier l'objet original. Chaque décorateur maintient une référence vers l'objet enveloppé et ajoute sa propre fonctionnalité. Cela vous permet de combiner plusieurs décorateurs pour des combinaisons de fonctionnalités flexibles sans créer de nombreuses sous-classes.

challenge icon

Défi

Moyen

Dans ce défi, vous allez implémenter le patron de conception Décorateur (Decorator) pour créer un système de commande de café flexible pour un café. Le patron Décorateur permet d'ajouter dynamiquement un comportement à des objets individuels, sans affecter le comportement des autres objets de la même classe.

Vous construisez un système pour un café qui doit :

  • Proposer différents types de boissons ("Espresso", "Dark Roast", "House Blend", "Decaf")
  • Permettre aux clients d'ajouter des condiments ("Milk", "Mocha", "Soy", "Whip") à leurs boissons
  • Calculer le coût des boissons avec les condiments ajoutés
  1. Complétez la classe abstraite Beverage et ses implémentations concrètes dans beverage.py
  2. Implémentez la classe abstraite CondimentDecorator et les décorateurs concrets dans condiment_decorator.py
  3. Implémentez des scénarios de test dans driver.py pour vérifier votre implémentation

Aide-mémoire

Le patron de conception Décorateur (Decorator Pattern) ajoute dynamiquement de nouvelles fonctionnalités aux objets sans modifier leur structure en enveloppant les objets pour étendre leur comportement.

Structure de base avec un exemple de café :

class Coffee:
    def cost(self):
        return 5
    
    def description(self):
        return "Simple coffee"

Créez des décorateurs qui enveloppent et étendent les fonctionnalités :

class MilkDecorator:
    def __init__(self, coffee):
        self.coffee = coffee
    
    def cost(self):
        return self.coffee.cost() + 2
    
    def description(self):
        return self.coffee.description() + " + Milk"

class SugarDecorator:
    def __init__(self, coffee):
        self.coffee = coffee
    
    def cost(self):
        return self.coffee.cost() + 1
    
    def description(self):
        return self.coffee.description() + " + Sugar"

Enchaînez les décorateurs pour construire des objets personnalisés :

# Commencer avec un café de base
my_coffee = Coffee()

# Ajouter des décorateurs
my_coffee = MilkDecorator(my_coffee)
my_coffee = SugarDecorator(my_coffee)

print(f"{my_coffee.description()}: ${my_coffee.cost()}")
# Résultat : Simple coffee + Milk + Sugar: $8

Exemple de formatage de texte :

class Text:
    def __init__(self, content):
        self.content = content
    
    def render(self):
        return self.content

class BoldDecorator:
    def __init__(self, text):
        self.text = text
    
    def render(self):
        return f"<b>{self.text.render()}</b>"

class ItalicDecorator:
    def __init__(self, text):
        self.text = text
    
    def render(self):
        return f"<i>{self.text.render()}</i>"

# Enchaîner les décorateurs
message = Text("Hello World")
message = BoldDecorator(message)
message = ItalicDecorator(message)

print(message.render())
# Résultat : <i><b>Hello World</b></i>

Avantages clés : permet des combinaisons de fonctionnalités flexibles sans créer de nombreuses sous-classes, maintient une référence à l'objet enveloppé et ajoute des fonctionnalités de manière dynamique.

Essayez vous-même

# Importer toutes les classes nécessaires
from beverage import Espresso, HouseBlend, DarkRoast, Decaf
from condiment_decorator import Milk, Mocha, Soy, Whip

# Gestionnaire de cas de test complet
test_case = input()

if test_case == "basic_beverage_test":
    # Tester la fonctionnalité de base de la boisson
    espresso = Espresso()
    print(f"Description: {espresso.get_description()}")
    print(f"Cost: ${espresso.cost():.2f}")

elif test_case == "all_beverages_test":
    # Tester tous les types de boissons
    beverages = [Espresso(), HouseBlend(), DarkRoast(), Decaf()]
    for beverage in beverages:
        print(f"{beverage.get_description()}: ${beverage.cost():.2f}")

elif test_case == "single_condiment_test":
    # Tester la décoration avec un seul condiment
    espresso = Espresso()
    espresso_with_milk = Milk(espresso)
    print(f"Description: {espresso_with_milk.get_description()}")
    print(f"Cost: ${espresso_with_milk.cost():.2f}")

elif test_case == "multiple_condiments_test":
    # Tester les décorations avec plusieurs condiments
    house_blend = HouseBlend()
    decorated_coffee = Mocha(Soy(Whip(house_blend)))
    print(f"Description: {decorated_coffee.get_description()}")
    print(f"Cost: ${decorated_coffee.cost():.2f}")

elif test_case == "all_condiments_test":
    # Tester tous les condiments sur une seule boisson
    dark_roast = DarkRoast()
    fully_loaded = Milk(Mocha(Soy(Whip(dark_roast))))
    print(f"Description: {fully_loaded.get_description()}")
    print(f"Cost: ${fully_loaded.cost():.2f}")

elif test_case == "cost_calculation_test":
    # Tester les calculs de coût pour différentes combinaisons
    espresso = Espresso()
    
    # Ajouts simples
    with_milk = Milk(espresso)
    with_mocha = Mocha(espresso)
    
    print(f"Espresso: ${espresso.cost():.2f}")
    print(f"Espresso + Milk: ${with_milk.cost():.2f}")
    print(f"Espresso + Mocha: ${with_mocha.cost():.2f}")

elif test_case == "decoration_order_test":
    # Tester que l'ordre de décoration n'affecte pas le résultat final
    decaf = Decaf()
    
    order1 = Milk(Mocha(decaf))
    order2 = Mocha(Milk(decaf))
    
    print(f"Order 1 - {order1.get_description()}: ${order1.cost():.2f}")
    print(f"Order 2 - {order2.get_description()}: ${order2.cost():.2f}")
    print(f"Same cost: {order1.cost() == order2.cost()}")

elif test_case == "complex_order_test":
    # Tester des commandes de boissons complexes
    orders = [
        Mocha(Whip(Espresso())),
        Soy(Milk(HouseBlend())),
        Whip(Mocha(Soy(DarkRoast()))),
        Milk(Decaf())
    ]
    
    for i, order in enumerate(orders, 1):
        print(f"Order {i}: {order.get_description()}")
        print(f"Cost: ${order.cost():.2f}")
        print("---")

elif test_case == "double_condiment_test":
    # Tester l'ajout du même condiment plusieurs fois
    espresso = Espresso()
    double_mocha = Mocha(Mocha(espresso))
    print(f"Description: {double_mocha.get_description()}")
    print(f"Cost: ${double_mocha.cost():.2f}")

elif test_case == "beverage_comparison_test":
    # Comparer les coûts de différentes boissons avec les mêmes condiments
    base_beverages = [Espresso(), HouseBlend(), DarkRoast(), Decaf()]
    
    print("All beverages with Milk + Mocha:")
    for beverage in base_beverages:
        decorated = Milk(Mocha(beverage))
        print(f"{decorated.get_description()}: ${decorated.cost():.2f}")

elif test_case == "condiment_cost_test":
    # Tester les coûts individuels des condiments
    espresso = Espresso()
    base_cost = espresso.cost()
    
    condiments = [
        ("Milk", Milk(espresso)),
        ("Mocha", Mocha(espresso)),
        ("Soy", Soy(espresso)),
        ("Whip", Whip(espresso))
    ]
    
    for name, decorated in condiments:
        additional_cost = decorated.cost() - base_cost
        print(f"{name} adds: ${additional_cost:.2f}")

elif test_case == "cheapest_most_expensive_test":
    # Trouver les boissons de base les moins chères et les plus chères
    beverages = [
        ("Espresso", Espresso()),
        ("House Blend", HouseBlend()),
        ("Dark Roast", DarkRoast()),
        ("Decaf", Decaf())
    ]
    
    beverages.sort(key=lambda x: x[1].cost())
    print(f"Cheapest: {beverages[0][0]} - ${beverages[0][1].cost():.2f}")
    print(f"Most Expensive: {beverages[-1][0]} - ${beverages[-1][1].cost():.2f}")

elif test_case == "nested_decoration_test":
    # Tester des décorations profondément imbriquées
    beverage = HouseBlend()
    # Ajouter 5 couches de décorations
    decorated = Whip(Soy(Mocha(Milk(Whip(beverage)))))
    print(f"Description: {decorated.get_description()}")
    print(f"Cost: ${decorated.cost():.2f}")
    
    # Compter le nombre de condiments
    description = decorated.get_description()
    condiment_count = description.count(" + ")
    print(f"Number of condiments: {condiment_count}")

elif test_case == "cost_breakdown_test":
    # Détail détaillé des coûts
    dark_roast = DarkRoast()
    print(f"Base Dark Roast: ${dark_roast.cost():.2f}")
    
    with_soy = Soy(dark_roast)
    print(f"+ Soy: ${with_soy.cost():.2f}")
    
    with_mocha = Mocha(with_soy)
    print(f"+ Mocha: ${with_mocha.cost():.2f}")
    
    with_whip = Whip(with_mocha)
    print(f"+ Whip: ${with_whip.cost():.2f}")
    
    final_cost = with_whip.cost()
    print(f"Final total: ${final_cost:.2f}")

elif test_case == "description_format_test":
    # Tester la cohérence du formatage de la description
    espresso = Espresso()
    test_combinations = [
        Milk(espresso),
        Mocha(Milk(espresso)),
        Whip(Mocha(Milk(espresso))),
        Soy(Whip(Mocha(Milk(espresso))))
    ]
    
    for combo in test_combinations:
        description = combo.get_description()
        print(f"Description: '{description}'")
        # Vérifier si la description commence par le nom de la boisson de base
        starts_correctly = description.startswith("Espresso")
        print(f"Starts with base name: {starts_correctly}")
        print("---")
quiz iconTestez-vous

Cette leçon comprend un petit quiz. Commencez la leçon pour y répondre et suivre votre progression.

Toutes les leçons de Object Oriented Programming