Prerequisites
- Basic understanding of programming concepts ๐
- Python installation (3.8+) ๐
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write clean, Pythonic code โจ
๐ฏ Introduction
Welcome to this exciting tutorial on the Decorator Pattern! ๐ In this guide, weโll explore how to dynamically extend object behavior without modifying their code.
Youโll discover how the decorator pattern can transform your Python development experience. Whether youโre building web applications ๐, creating logging systems ๐, or adding features to existing objects ๐จ, understanding the decorator pattern is essential for writing flexible, maintainable code.
By the end of this tutorial, youโll feel confident using the decorator pattern in your own projects! Letโs dive in! ๐โโ๏ธ
๐ Understanding Decorator Pattern
๐ค What is the Decorator Pattern?
The decorator pattern is like adding toppings to your pizza ๐. Think of it as wrapping gifts ๐ - you start with a basic object and keep adding layers of functionality around it!
In Python terms, the decorator pattern allows you to wrap objects with additional behavior without altering their structure. This means you can:
- โจ Add new features dynamically
- ๐ Extend functionality at runtime
- ๐ก๏ธ Keep original objects unchanged
๐ก Why Use Decorator Pattern?
Hereโs why developers love the decorator pattern:
- Open/Closed Principle ๐: Classes open for extension, closed for modification
- Single Responsibility ๐ป: Each decorator handles one concern
- Runtime Flexibility ๐: Add/remove features dynamically
- Composable Behavior ๐ง: Mix and match decorators
Real-world example: Imagine building a coffee shop system โ. With the decorator pattern, you can add milk, sugar, or whipped cream without changing the base coffee class!
๐ง Basic Syntax and Usage
๐ Simple Example
Letโs start with a friendly example:
# ๐ Hello, Decorator Pattern!
from abc import ABC, abstractmethod
# ๐จ Component interface
class Coffee(ABC):
@abstractmethod
def cost(self) -> float:
pass
@abstractmethod
def description(self) -> str:
pass
# โ Concrete component
class SimpleCoffee(Coffee):
def cost(self) -> float:
return 2.0 # ๐ต Base coffee price
def description(self) -> str:
return "Simple coffee"
# ๐ Base decorator
class CoffeeDecorator(Coffee):
def __init__(self, coffee: Coffee):
self._coffee = coffee # ๐ฆ Wrapped component
def cost(self) -> float:
return self._coffee.cost()
def description(self) -> str:
return self._coffee.description()
๐ก Explanation: Notice how we create a base decorator that wraps any Coffee object! The decorator implements the same interface as the component.
๐ฏ Common Patterns
Here are patterns youโll use daily:
# ๐๏ธ Pattern 1: Concrete decorators
class MilkDecorator(CoffeeDecorator):
def cost(self) -> float:
return self._coffee.cost() + 0.5 # ๐ฅ Add milk cost
def description(self) -> str:
return f"{self._coffee.description()} + milk"
# ๐จ Pattern 2: Sugar decorator
class SugarDecorator(CoffeeDecorator):
def cost(self) -> float:
return self._coffee.cost() + 0.2 # ๐ฏ Add sugar cost
def description(self) -> str:
return f"{self._coffee.description()} + sugar"
# ๐ Pattern 3: Using decorators
coffee = SimpleCoffee() # โ Start with simple coffee
coffee = MilkDecorator(coffee) # ๐ฅ Add milk
coffee = SugarDecorator(coffee) # ๐ฏ Add sugar
print(f"โ {coffee.description()}")
print(f"๐ฐ Total: ${coffee.cost()}")
๐ก Practical Examples
๐ Example 1: E-commerce Product Features
Letโs build something real:
# ๐๏ธ Define our product interface
class Product(ABC):
@abstractmethod
def get_price(self) -> float:
pass
@abstractmethod
def get_description(self) -> str:
pass
# ๐ฑ Concrete product
class Smartphone(Product):
def __init__(self, model: str, base_price: float):
self.model = model
self.base_price = base_price
def get_price(self) -> float:
return self.base_price
def get_description(self) -> str:
return f"๐ฑ {self.model}"
# ๐ Product decorator base
class ProductDecorator(Product):
def __init__(self, product: Product):
self._product = product
# ๐ก๏ธ Warranty decorator
class WarrantyDecorator(ProductDecorator):
def __init__(self, product: Product, years: int):
super().__init__(product)
self.years = years
def get_price(self) -> float:
warranty_cost = 50 * self.years # ๐ต $50 per year
return self._product.get_price() + warranty_cost
def get_description(self) -> str:
return f"{self._product.get_description()} + {self.years}-year warranty ๐ก๏ธ"
# ๐จ Case decorator
class CaseDecorator(ProductDecorator):
def __init__(self, product: Product, case_type: str):
super().__init__(product)
self.case_type = case_type
def get_price(self) -> float:
case_prices = {
"basic": 15,
"premium": 35,
"luxury": 60
}
return self._product.get_price() + case_prices.get(self.case_type, 15)
def get_description(self) -> str:
return f"{self._product.get_description()} + {self.case_type} case ๐ฆ"
# ๐ฎ Let's use it!
phone = Smartphone("iPhone 15", 999)
phone = WarrantyDecorator(phone, 2) # ๐ก๏ธ Add 2-year warranty
phone = CaseDecorator(phone, "premium") # ๐ฆ Add premium case
print(f"Product: {phone.get_description()}")
print(f"Total Price: ${phone.get_price()}")
๐ฏ Try it yourself: Add a screen protector decorator and a fast charging adapter decorator!
๐ฎ Example 2: Game Character Abilities
Letโs make it fun:
# ๐ Character ability system
class Character(ABC):
@abstractmethod
def get_stats(self) -> dict:
pass
@abstractmethod
def get_abilities(self) -> list:
pass
# ๐ก๏ธ Basic character
class Warrior(Character):
def __init__(self, name: str):
self.name = name
def get_stats(self) -> dict:
return {
"name": self.name,
"health": 100,
"attack": 20,
"defense": 15,
"speed": 10
}
def get_abilities(self) -> list:
return ["โ๏ธ Basic Attack"]
# ๐ฏ Ability decorator base
class AbilityDecorator(Character):
def __init__(self, character: Character):
self._character = character
# ๐ฅ Fire enchantment
class FireEnchantment(AbilityDecorator):
def get_stats(self) -> dict:
stats = self._character.get_stats().copy()
stats["attack"] += 15 # ๐ฅ +15 fire damage
stats["fire_damage"] = True
return stats
def get_abilities(self) -> list:
abilities = self._character.get_abilities().copy()
abilities.append("๐ฅ Fire Strike")
return abilities
# โ๏ธ Ice armor
class IceArmor(AbilityDecorator):
def get_stats(self) -> dict:
stats = self._character.get_stats().copy()
stats["defense"] += 20 # โ๏ธ +20 ice defense
stats["speed"] -= 5 # Slower but tankier
return stats
def get_abilities(self) -> list:
abilities = self._character.get_abilities().copy()
abilities.append("โ๏ธ Frost Shield")
return abilities
# ๐ฎ Create epic warrior!
hero = Warrior("Aragorn")
hero = FireEnchantment(hero) # ๐ฅ Add fire power
hero = IceArmor(hero) # โ๏ธ Add ice defense
print(f"๐ฎ Character: {hero.get_stats()['name']}")
print(f"๐ Stats: {hero.get_stats()}")
print(f"๐ฏ Abilities: {', '.join(hero.get_abilities())}")
๐ Advanced Concepts
๐งโโ๏ธ Advanced Topic 1: Dynamic Decorator Chains
When youโre ready to level up, try this advanced pattern:
# ๐ฏ Advanced decorator manager
class DecoratorChain:
def __init__(self, base_object):
self.base = base_object
self.decorators = []
def add_decorator(self, decorator_class, *args, **kwargs):
"""โจ Dynamically add decorators"""
current = self.base
for dec in self.decorators:
current = dec
new_decorator = decorator_class(current, *args, **kwargs)
self.decorators.append(new_decorator)
return self
def get_object(self):
"""๐ Get the fully decorated object"""
current = self.base
for dec in self.decorators:
current = dec
return current
# ๐ช Using the chain
coffee_chain = DecoratorChain(SimpleCoffee())
coffee_chain.add_decorator(MilkDecorator)
coffee_chain.add_decorator(SugarDecorator)
final_coffee = coffee_chain.get_object()
print(f"โ {final_coffee.description()}: ${final_coffee.cost()}")
๐๏ธ Advanced Topic 2: Conditional Decorators
For the brave developers:
# ๐ Smart decorator system
class ConditionalDecorator(CoffeeDecorator):
def __init__(self, coffee: Coffee, condition: callable):
super().__init__(coffee)
self.condition = condition
self.active = False
def activate_if(self):
"""โจ Activate decorator based on condition"""
self.active = self.condition()
return self
def cost(self) -> float:
if self.active:
return self._apply_cost()
return self._coffee.cost()
def _apply_cost(self) -> float:
return self._coffee.cost() # Override in subclasses
# ๐ซ Time-based decorator
import datetime
class HappyHourDecorator(ConditionalDecorator):
def _apply_cost(self) -> float:
return self._coffee.cost() * 0.8 # ๐ 20% off!
def description(self) -> str:
if self.active:
return f"{self._coffee.description()} (Happy Hour! ๐)"
return self._coffee.description()
# Check if it's happy hour (3-6 PM)
def is_happy_hour():
hour = datetime.datetime.now().hour
return 15 <= hour < 18
coffee = SimpleCoffee()
coffee = HappyHourDecorator(coffee, is_happy_hour).activate_if()
print(f"โ {coffee.description()}: ${coffee.cost()}")
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Decorator Order Matters
# โ Wrong way - order can affect behavior!
coffee1 = SimpleCoffee()
coffee1 = MilkDecorator(coffee1) # First milk
coffee1 = SugarDecorator(coffee1) # Then sugar
coffee2 = SimpleCoffee()
coffee2 = SugarDecorator(coffee2) # First sugar
coffee2 = MilkDecorator(coffee2) # Then milk
# Description order will be different! ๐ฐ
# โ
Correct way - be consistent with order!
def create_coffee_with_extras(extras: list):
coffee = SimpleCoffee()
# Apply decorators in consistent order
decorator_map = {
"milk": MilkDecorator,
"sugar": SugarDecorator
}
for extra in sorted(extras): # ๐ฏ Sort for consistency
if extra in decorator_map:
coffee = decorator_map[extra](coffee)
return coffee
๐คฏ Pitfall 2: Forgetting the Interface
# โ Dangerous - decorator doesn't match interface!
class BadDecorator: # ๐ฑ Doesn't inherit from Coffee!
def __init__(self, coffee):
self.coffee = coffee
def get_price(self): # Wrong method name!
return self.coffee.cost() + 1
# โ
Safe - always inherit and implement interface!
class GoodDecorator(CoffeeDecorator):
def cost(self) -> float:
return self._coffee.cost() + 1 # โ
Correct method!
def description(self) -> str:
return f"{self._coffee.description()} + extra" # โ
All methods implemented!
๐ ๏ธ Best Practices
- ๐ฏ Keep Decorators Focused: Each decorator should do one thing well
- ๐ Maintain the Interface: Always implement all abstract methods
- ๐ก๏ธ Use Type Hints: Make your code self-documenting
- ๐จ Name Clearly:
EmailNotificationDecorator
notEND
- โจ Document Behavior: Explain what each decorator adds
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Pizza Ordering System
Create a decorator-based pizza customization system:
๐ Requirements:
- โ Base pizza types (Margherita, Pepperoni, Veggie)
- ๐ท๏ธ Topping decorators (cheese, mushrooms, olives)
- ๐ค Size decorators (small, medium, large)
- ๐ Special crust options (thin, thick, stuffed)
- ๐จ Each pizza needs an emoji representation!
๐ Bonus Points:
- Add discount decorators for combos
- Implement calorie tracking
- Create a receipt generator
๐ก Solution
๐ Click to see solution
# ๐ฏ Our decorator-based pizza system!
from abc import ABC, abstractmethod
class Pizza(ABC):
@abstractmethod
def cost(self) -> float:
pass
@abstractmethod
def description(self) -> str:
pass
@abstractmethod
def calories(self) -> int:
pass
# ๐ Base pizzas
class Margherita(Pizza):
def cost(self) -> float:
return 8.99
def description(self) -> str:
return "๐ Margherita"
def calories(self) -> int:
return 800
# ๐ Pizza decorator base
class PizzaDecorator(Pizza):
def __init__(self, pizza: Pizza):
self._pizza = pizza
# ๐ง Extra cheese decorator
class ExtraCheeseDecorator(PizzaDecorator):
def cost(self) -> float:
return self._pizza.cost() + 2.00
def description(self) -> str:
return f"{self._pizza.description()} + extra cheese ๐ง"
def calories(self) -> int:
return self._pizza.calories() + 200
# ๐ Mushroom decorator
class MushroomDecorator(PizzaDecorator):
def cost(self) -> float:
return self._pizza.cost() + 1.50
def description(self) -> str:
return f"{self._pizza.description()} + mushrooms ๐"
def calories(self) -> int:
return self._pizza.calories() + 50
# ๐ Size decorator
class SizeDecorator(PizzaDecorator):
def __init__(self, pizza: Pizza, size: str):
super().__init__(pizza)
self.size = size
self.multipliers = {
"small": 0.8,
"medium": 1.0,
"large": 1.5,
"family": 2.0
}
def cost(self) -> float:
multiplier = self.multipliers.get(self.size, 1.0)
return self._pizza.cost() * multiplier
def description(self) -> str:
size_emoji = {
"small": "๐ข",
"medium": "๐ก",
"large": "๐ ",
"family": "๐ด"
}
emoji = size_emoji.get(self.size, "๐ก")
return f"{emoji} {self.size.title()} {self._pizza.description()}"
def calories(self) -> int:
multiplier = self.multipliers.get(self.size, 1.0)
return int(self._pizza.calories() * multiplier)
# ๐ฎ Create custom pizza!
my_pizza = Margherita()
my_pizza = SizeDecorator(my_pizza, "large")
my_pizza = ExtraCheeseDecorator(my_pizza)
my_pizza = MushroomDecorator(my_pizza)
print(f"๐ Order: {my_pizza.description()}")
print(f"๐ฐ Total: ${my_pizza.cost():.2f}")
print(f"๐ฅ Calories: {my_pizza.calories()}")
# ๐งพ Receipt generator
def generate_receipt(pizza: Pizza):
print("\n" + "="*40)
print("๐งพ PIZZA RECEIPT")
print("="*40)
print(f"Order: {pizza.description()}")
print(f"Price: ${pizza.cost():.2f}")
print(f"Calories: {pizza.calories()} cal")
print("="*40)
print("Thank you for your order! ๐")
generate_receipt(my_pizza)
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Create decorators that extend object behavior dynamically ๐ช
- โ Avoid modifying existing classes while adding features ๐ก๏ธ
- โ Apply the pattern in real-world scenarios ๐ฏ
- โ Debug decorator chains like a pro ๐
- โ Build flexible systems with Python! ๐
Remember: The decorator pattern is your friend for creating extensible, maintainable code! ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered the decorator pattern!
Hereโs what to do next:
- ๐ป Practice with the pizza system exercise
- ๐๏ธ Apply decorators to your existing projects
- ๐ Move on to our next tutorial: Strategy Pattern
- ๐ Share your decorator creations with others!
Remember: Every design pattern expert was once a beginner. Keep coding, keep learning, and most importantly, have fun! ๐
Happy decorating! ๐๐โจ