- Published on
[Structural] Decorator Design Pattern
🎨 Decorator Design Pattern
The Decorator Pattern is a structural design pattern that lets you add new behavior to objects dynamically—without modifying their original class. It does this by wrapping objects in decorator classes that implement the same interface.
🧠 Core Idea (Intuition)
Instead of creating many subclasses like:
Coffee
├─ CoffeeWithMilk
├─ CoffeeWithSugar
├─ CoffeeWithMilkAndSugar
├─ CoffeeWithMilkSugarCaramel
You compose behavior at runtime:
Coffee → MilkDecorator → SugarDecorator → CaramelDecorator
This keeps classes small, flexible, and open for extension.
🧩 Structure
The pattern has four key roles:
Component
- Common interface for objects and decorators
ConcreteComponent
- The original object
Decorator (abstract)
- Wraps a Component
ConcreteDecorator
- Adds new behavior
✅ When to Use Decorator
Use it when:
- You want to add responsibilities dynamically
- Subclassing would cause a class explosion
- You want to follow Open–Closed Principle
- Behavior combinations should be flexible at runtime
Avoid it when:
- You need strict object identity
- Debugging layered behavior would be too complex
🐍 Python Example (Clean & Idiomatic)
1️⃣ Component Interface
from abc import ABC, abstractmethod
class Coffee(ABC):
@abstractmethod
def cost(self) -> float:
pass
@abstractmethod
def description(self) -> str:
pass
2️⃣ Concrete Component
class SimpleCoffee(Coffee):
def cost(self) -> float:
return 2.0
def description(self) -> str:
return "Simple coffee"
3️⃣ Base Decorator
class CoffeeDecorator(Coffee):
def __init__(self, coffee: Coffee):
self._coffee = coffee
def cost(self) -> float:
return self._coffee.cost()
def description(self) -> str:
return self._coffee.description()
4️⃣ Concrete Decorators
class MilkDecorator(CoffeeDecorator):
def cost(self) -> float:
return self._coffee.cost() + 0.5
def description(self) -> str:
return self._coffee.description() + ", milk"
class SugarDecorator(CoffeeDecorator):
def cost(self) -> float:
return self._coffee.cost() + 0.2
def description(self) -> str:
return self._coffee.description() + ", sugar"
5️⃣ Usage
coffee = SimpleCoffee()
coffee = MilkDecorator(coffee)
coffee = SugarDecorator(coffee)
print(coffee.description()) # Simple coffee, milk, sugar
print(coffee.cost()) # 2.7
🎯 Decorators stack cleanly and can be reordered or skipped.
🔍 Decorator vs Inheritance
| Aspect | Decorator | Inheritance |
|---|---|---|
| Runtime flexibility | ✅ High | ❌ Low |
| Class explosion | ❌ Avoided | ✅ Common |
| Open–Closed Principle | ✅ Yes | ❌ Often violated |
| Object identity | ❌ Wrapped | ✅ Preserved |
⚙️ Real-World Examples
- Java I/O Streams (
BufferedInputStream) - GUI toolkits (scrollbars, borders)
- Middleware pipelines
- Python function decorators (
@lru_cache,@retry) - Game buffs / debuffs (very common in RPG engines)
🧠 Mental Model
Inheritance adds behavior vertically. Decorator adds behavior horizontally.