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.
Défi
MoyenDans 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
- Complétez la classe abstraite
Beverageet ses implémentations concrètes dansbeverage.py - Implémentez la classe abstraite
CondimentDecoratoret les décorateurs concrets danscondiment_decorator.py - Implémentez des scénarios de test dans
driver.pypour 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: $8Exemple 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("---")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
1Les fondamentaux de la POO
Fichiers externesIntroduction à la POOClasses vs ObjetsLe paramètre selfMéthodesAttributsMéthode constructeur (__init__)Récapitulatif - Calculatrice simple4Héritage
Héritage de baseLa fonction super()Redéfinition de méthodeHéritage multipleOrdre de résolution des méthodesRécapitulatif - Hiérarchie des employés7Méthodes spéciales
Introduction aux méthodes magiquesSurcharge d'opérateursMéthodes magiques de conteneurRécapitulatif - Liste personnalisée10Patrons de conception, Partie 1
Introduction aux patrons de conceptionPatron SingletonPatron FactoryPatron ObserverPatron Strategy2Décorateurs
Introduction aux décorateursDécorateur de propriétéDécorateur de méthode statiqueDécorateur de méthode de classe5Polymorphisme
Retour sur la redéfinition de méthodeDuck TypingClasses et méthodes abstraitesConception d'interfacesRécapitulatif - Calculateur de formes8Concepts avancés de la POO
Composition vs HéritageMixinsMéthodes statiques et de classeDécorateurs de classeGestionnaires de contexte11Patrons de conception, Partie 2
Patron CommandePatron AdaptateurPatron DécorateurPatron Méthode TemplatePatron ÉtatPatron Composite3Propriétés de classe
Variables d'instance vs variables de classeDécorateurs de propriétéAttributs privésRécapitulatif - Gestionnaire de compte bancaire