Decorator Pattern
The Decorator pattern attaches new behavior to objects by wrapping them in decorator objects. Each decorator adds responsibilities before or after delegating to the wrapped object. The key: decorators have the same interface as what they wrap.
Quick Reference
Use When
- You need mix-and-match behavior without subclass explosion
- Behavior must be added/removed at runtime
- You want transparent wrapping (same interface as wrapped object)
- Multiple objects need different combinations of behaviors
Avoid When
- Only one or two fixed variations needed (inheritance is simpler)
- Performance is critical (each decorator adds indirection)
- The component interface is very large (too many methods to delegate)
- You need to change the interface (use Adapter instead)
The Analogy
Wrapping a gift: you start with the present, add tissue paper, then wrapping paper, then a ribbon. Each layer adds something without changing the gift inside. You can mix and match layers in any order.
The Problem
You want to add behavior to individual objects, not entire classes. Inheritance is static and affects all instances. Subclassing for every combination of behaviors leads to a class explosion.
The Solution
Wrap objects in decorator objects that add behavior before or after delegating to the wrapped object. Decorators have the same interface as the objects they wrap, so they are interchangeable.
Structure
Shows the static relationships between classes in the pattern.
Pattern Variants
Object Decorator
Wraps objects by implementing the same interface and delegating to the wrapped instance. The classic GoF pattern.
Function Decorator
Wraps functions by returning a new function that calls the original. Common in Python and functional programming.
Middleware Chain
Decorator pattern applied to request handlers. Each middleware wraps the next, forming a chain (Express, Redux).
Implementations
Copy-paste examples in Python, JavaScript, and GDScript. Each includes validation and Director patterns.
from abc import ABC, abstractmethod
import functools
import time
# ===== OBJECT DECORATOR (Classic GoF Pattern) =====
class DataSource(ABC):
@abstractmethod
def read(self) -> str:
pass
@abstractmethod
def write(self, data: str) -> None:
pass
class FileDataSource(DataSource):
def __init__(self, filename: str):
self.filename = filename
self._data = ""
def read(self) -> str:
print(f"Reading from {self.filename}")
return self._data
def write(self, data: str) -> None:
print(f"Writing to {self.filename}")
self._data = data
class DataSourceDecorator(DataSource):
def __init__(self, source: DataSource):
self._source = source
def read(self) -> str:
return self._source.read()
def write(self, data: str) -> None:
self._source.write(data)
class EncryptionDecorator(DataSourceDecorator):
def read(self) -> str:
data = super().read()
print("Decrypting...")
return self._decrypt(data)
def write(self, data: str) -> None:
print("Encrypting...")
super().write(self._encrypt(data))
def _encrypt(self, data: str) -> str:
# Simple ROT13 for demonstration
return data.translate(str.maketrans(
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
'NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm'))
def _decrypt(self, data: str) -> str:
return self._encrypt(data) # ROT13 is symmetric
class LoggingDecorator(DataSourceDecorator):
def read(self) -> str:
print(f"[LOG] read() called at {time.strftime('%H:%M:%S')}")
return super().read()
def write(self, data: str) -> None:
print(f"[LOG] write({len(data)} bytes) at {time.strftime('%H:%M:%S')}")
super().write(data)
# Usage: stack decorators - ORDER MATTERS!
source = FileDataSource("data.txt")
source = EncryptionDecorator(source) # Inner: handles encrypt/decrypt
source = LoggingDecorator(source) # Outer: logs before encrypt, after decrypt
source.write("Secret message")
print(source.read())
# ===== FUNCTION DECORATOR (Pythonic approach) =====
def timing(func):
"""Decorator that measures execution time."""
@functools.wraps(func) # Preserves __name__, __doc__
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
def retry(max_attempts: int = 3, delay: float = 1.0):
"""Decorator factory for retry logic."""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts - 1:
raise
print(f"Attempt {attempt + 1} failed: {e}, retrying...")
time.sleep(delay)
return wrapper
return decorator
@timing
@retry(max_attempts=3)
def fetch_data(url: str) -> str:
# Simulated API call
return f"Data from {url}"Two approaches: Object Decorator (classic GoF with DataSource interface) and Function Decorator (Pythonic @decorator syntax). Shows encryption, logging, timing, and retry patterns. Note: @functools.wraps preserves function metadata.
// ===== OBJECT DECORATOR (Modern browsers + Node.js 18+) =====
class HttpClient {
async fetch(url) {
console.log(`Fetching ${url}`);
// Simulated response
return { data: "response", status: 200 };
}
}
class HttpClientDecorator {
constructor(client) {
this.client = client;
}
async fetch(url) {
return this.client.fetch(url);
}
}
class CachingDecorator extends HttpClientDecorator {
constructor(client, ttlMs = 60000) {
super(client);
this.cache = new Map();
this.ttl = ttlMs;
}
async fetch(url) {
const cached = this.cache.get(url);
if (cached && Date.now() - cached.timestamp < this.ttl) {
console.log(`[CACHE HIT] ${url}`);
return cached.data;
}
console.log(`[CACHE MISS] ${url}`);
const result = await super.fetch(url);
this.cache.set(url, { data: result, timestamp: Date.now() });
return result;
}
}
class RetryDecorator extends HttpClientDecorator {
constructor(client, maxRetries = 3, delayMs = 1000) {
super(client);
this.maxRetries = maxRetries;
this.delay = delayMs;
}
async fetch(url) {
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
try {
return await super.fetch(url);
} catch (err) {
console.log(`Attempt ${attempt} failed: ${err.message}`);
if (attempt === this.maxRetries) throw err;
await new Promise(r => setTimeout(r, this.delay));
}
}
}
}
class LoggingDecorator extends HttpClientDecorator {
async fetch(url) {
const start = performance.now();
console.log(`[LOG] Request: ${url}`);
const result = await super.fetch(url);
console.log(`[LOG] Response: ${result.status} (${(performance.now() - start).toFixed(1)}ms)`);
return result;
}
}
// Usage: Order matters! Outermost runs first.
let client = new HttpClient();
client = new RetryDecorator(client); // Inner: retry on failure
client = new CachingDecorator(client); // Middle: cache successful responses
client = new LoggingDecorator(client); // Outer: log all requests (even cached)
await client.fetch("/api/users");
// ===== FUNCTION DECORATOR (Functional approach) =====
function withTiming(fn) {
return async function(...args) {
const start = performance.now();
const result = await fn.apply(this, args);
console.log(`${fn.name || 'fn'} took ${(performance.now() - start).toFixed(2)}ms`);
return result;
};
}
function withAuth(token) {
return function(fn) {
return async function(url, options = {}) {
return fn.call(this, url, {
...options,
headers: { ...options.headers, Authorization: `Bearer ${token}` }
});
};
};
}
// Compose decorators
const secureFetch = withTiming(withAuth("token123")(fetch));Two approaches: Object Decorator (HttpClient with caching, retry, logging) and Function Decorator (higher-order functions). Uses standard APIs (performance.now(), fetch) available in modern browsers and Node.js 18+.
# weapon.gd - Component interface
class_name Weapon
extends RefCounted
func get_damage() -> int:
return 0
func get_name() -> String:
return ""
# sword.gd - Concrete component
class_name Sword
extends Weapon
func get_damage() -> int:
return 10
func get_name() -> String:
return "Sword"
# weapon_decorator.gd - Base decorator
class_name WeaponDecorator
extends Weapon
var _weapon: Weapon
func _init(weapon: Weapon) -> void:
_weapon = weapon
func get_damage() -> int:
return _weapon.get_damage()
func get_name() -> String:
return _weapon.get_name()
# fire_enchant.gd - Adds flat damage
class_name FireEnchant
extends WeaponDecorator
func get_damage() -> int:
return _weapon.get_damage() + 5
func get_name() -> String:
return "Flaming " + _weapon.get_name()
# sharpened.gd - Multiplies damage by 1.5x
class_name Sharpened
extends WeaponDecorator
func get_damage() -> int:
return int(_weapon.get_damage() * 1.5)
func get_name() -> String:
return "Sharp " + _weapon.get_name()
# ===== ORDER MATTERS! =====
# Different stacking order = different damage
func _ready() -> void:
var sword = Sword.new() # Base: 10 damage
# Order A: Sharpen first, then add fire
var order_a = FireEnchant.new(Sharpened.new(sword))
# Calculation: (10 * 1.5) + 5 = 15 + 5 = 20
# Order B: Add fire first, then sharpen
var order_b = Sharpened.new(FireEnchant.new(sword))
# Calculation: (10 + 5) * 1.5 = 15 * 1.5 = 22
print("%s: %d damage" % [order_a.get_name(), order_a.get_damage()])
# Output: Flaming Sharp Sword: 20 damage
print("%s: %d damage" % [order_b.get_name(), order_b.get_damage()])
# Output: Sharp Flaming Sword: 22 damage
# The outer decorator runs LAST on the computed value.
# Fire(Sharp(Sword)): 10 -> *1.5 -> +5 = 20
# Sharp(Fire(Sword)): 10 -> +5 -> *1.5 = 22GDScript Decorator for weapon enchantments showing ORDER SENSITIVITY. Sharpened multiplies (1.5x), FireEnchant adds (+5). Different stacking order produces different damage: Fire(Sharp(Sword))=20 vs Sharp(Fire(Sword))=22.
Decorator Pattern vs Proxy
| Aspect | Decorator Pattern | Proxy |
|---|---|---|
| Intent | Add behavior (smart wrapper) | Control access (lazy load, auth) |
| Interface | Keeps same interface | Keeps same interface |
| Stacking | Multiple decorators composable | Usually single wrapper |
| API contract | Keeps same interface | Changes interface |
| Purpose | Enhance existing behavior | Make incompatible interfaces work together |
| Where behavior changes | Outside (wraps the object) | Inside (swaps the algorithm) |
Real-World Examples
- I/O streams: wrapping streams with buffering, compression, encryption
- Web frameworks: Express/Next.js middleware wrapping request handlers
- GUI: adding scrollbars, borders, or shadows to components
- Logging/timing: wrapping functions to measure execution time or add tracing
- Caching: wrapping API clients to add cache layers
- Retry logic: wrapping unstable services with exponential backoff
- Game buffs/debuffs: stacking stat modifiers on characters
Common Mistakes
Wrong stacking order for encrypt + compress
// WRONG: Compressing encrypted data is inefficient
source = new EncryptionDecorator(source); // Encrypt first
source = new CompressionDecorator(source); // Then compress random bytes - bad!
// CORRECT: Compress first, then encrypt the smaller data
source = new CompressionDecorator(source); // Compress first
source = new EncryptionDecorator(source); // Then encryptFix: Always compress BEFORE encrypting. Encrypted data looks random and compresses poorly.
Breaking identity checks
const original = new DataSource();
const decorated = new LoggingDecorator(original);
console.log(original === decorated); // false!
set.has(decorated); // false if set contains originalFix: Document that decorated objects have different identity. Avoid identity checks, or keep references to originals.
Forgetting @functools.wraps in Python
def timer(func):
def wrapper(*args, **kwargs):
# ...
return wrapper
@timer
def my_function():
"""My docstring."""
pass
print(my_function.__name__) # "wrapper" - wrong!
print(my_function.__doc__) # None - docstring lost!Fix: Always use @functools.wraps(func) to preserve __name__, __doc__, and other metadata.
Testing decorator chains, not individual decorators
// Testing LoggingDecorator + CachingDecorator + RetryDecorator together
// If test fails, which decorator broke?Fix: Test each decorator in isolation with a mock component. Only test 1-2 common combinations.
When to Use Decorator Pattern
- When you need to add behavior to objects without affecting other objects of the same class
- When extension by subclassing is impractical or impossible
- When you need to add and remove responsibilities at runtime
- When you want to combine behaviors in many different ways
- When you need transparent wrapping that preserves the original interface
- When building middleware chains (Express, Redux, HTTP clients)
Pitfalls to Avoid
- Order sensitivity: decorator order matters and can produce unexpected results (see GDScript example)
- Identity issues: decorated objects are not identical to the original (a !== decorator(a))
- Interface bloat: all decorators must implement the entire component interface
- Debugging complexity: many wrapper layers make stack traces hard to read ("Decorator Soup")
- Performance overhead: each decorator adds a layer of indirection
- Metadata loss: function decorators can lose __name__, __doc__ (use @functools.wraps in Python)
Frequently Asked Questions
What is the difference between Decorator pattern and Python decorators?
Python @decorators are a language feature for wrapping functions (syntax sugar for func = decorator(func)). The Decorator pattern is a design pattern for wrapping objects behind a shared interface. They share the wrapping concept: both add behavior without modifying the original. Python decorators are often simpler for functions; the full pattern is useful for object hierarchies.
What is the difference between Decorator pattern and TypeScript/JavaScript decorators?
JavaScript has a Stage 3 TC39 decorators proposal for class/method decoration, different from the legacy TypeScript "experimentalDecorators". These are language features for metaprogramming (modifying classes at definition time). The Decorator design pattern wraps objects at runtime. Many libraries (NestJS, Angular) use legacy TS decorators, not standard JS decorators yet.
Is middleware a Decorator pattern?
Yes, middleware is Decorator applied to request handlers. Each middleware wraps the next handler, adding behavior (logging, auth, parsing) before/after the core logic. Express middleware, Redux middleware, and HTTP client interceptors all follow this pattern. The key insight: middleware order matters just like decorator stacking order.
Why does decorator order matter?
Decorators apply from inside out. If Decorator A multiplies and Decorator B adds, then B(A(x)) gives different results than A(B(x)). Example: Sharp(Fire(Sword)) = (10+5)*1.5 = 22, but Fire(Sharp(Sword)) = (10*1.5)+5 = 20. For encryption/compression: always compress BEFORE encrypting (encrypted data looks random and compresses poorly).
How is Decorator different from Proxy?
Intent differs: Decorator adds behavior (logging, timing, retries), Proxy controls access (lazy loading, auth, remote access). Decorators are typically stackable (multiple wrappers), while Proxy usually has one wrapper. Both maintain the same interface as the wrapped object.
How do I test decorated objects?
Test each decorator in isolation: (1) Create a mock component with known output, (2) Wrap it with one decorator, (3) Assert the output changed correctly and the mock was called once, (4) For stacks, test 1-2 common combinations, not every permutation.