Decorator
Attaches additional responsibilities to an object dynamically.
Decorator Pattern: A Complete Guide with Examples in 11 Programming Languages
The Decorator Pattern is a structural design pattern that allows you to dynamically add new functionality to objects by wrapping them in decorator objects. This pattern provides a flexible alternative to subclassing for extending functionality, enabling you to add responsibilities to individual objects without affecting other objects of the same class. Whether you're building text formatting systems, adding features to UI components, or creating middleware pipelines, the Decorator Pattern provides an elegant way to compose behavior at runtime.
In this comprehensive guide, we'll explore the Decorator Pattern with real-world examples in Python, TypeScript, Java, JavaScript, C#, PHP, Go, Rust, Dart, Swift, and Kotlin - complete with language-specific best practices, common pitfalls, and when to use this powerful pattern.
Table of Contents
- What is the Decorator Pattern?
- Why Use the Decorator Pattern?
- Decorator Pattern Comparison
- Decorator Pattern Explained
- Class Diagram
- Beginner-Friendly Example
- Production-Ready Example
- Real-World Use Cases
- Language-Specific Mistakes and Anti-Patterns
- Frequently Asked Questions
- Key Takeaways
What is the Decorator Pattern?
The Decorator Pattern attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality. Instead of using inheritance to add features, you wrap the original object with decorator objects that add new behavior while keeping the same interface.
Think of it like getting dressed: you start with a base layer (T-shirt), then add decorators (jacket, coat, scarf) based on the weather. Each layer adds functionality (warmth, protection) without changing the fact that you're still a person wearing clothes. You can add or remove layers dynamically, and the combinations are flexible.
Why Use the Decorator Pattern?
The Decorator Pattern offers several compelling benefits:
- Flexible Extension: Add responsibilities to objects dynamically at runtime
- Alternative to Subclassing: Avoid class explosion from creating subclasses for every combination
- Single Responsibility Principle: Each decorator focuses on one specific enhancement
- Open/Closed Principle: Open for extension, closed for modification
- Composable Behavior: Mix and match decorators to create complex functionality
- Transparent to Clients: Decorated objects maintain the same interface
- Runtime Configuration: Choose which features to apply at runtime
Decorator Pattern Comparison
Let's compare Decorator with related patterns:
| Pattern | Purpose | When Applied | Combination | Interface |
|---|---|---|---|---|
| Decorator | Add responsibilities dynamically | Runtime | Stack multiple decorators | Same as component |
| Composite | Tree structures, treat uniformly | Design time | Hierarchical composition | Same as component |
| Proxy | Control access, add indirection | Runtime | Single proxy | Same as subject |
| Adapter | Convert interface | After building | Single adapter | Different interface |
| Strategy | Change algorithm | Runtime | Single strategy | Different interface |
Key Distinction: Decorator adds responsibilities while keeping the same interface; Adapter changes the interface; Proxy controls access.
Decorator Pattern Explained
The Decorator Pattern typically involves:
Core Components:
- Component: Interface defining operations that can be decorated
- ConcreteComponent: Base implementation that can be decorated
- Decorator: Abstract class that wraps a Component and implements the Component interface
- ConcreteDecorator: Specific decorator that adds responsibilities
Key Relationships:
- Decorator contains (wraps) a Component reference
- Decorator implements the Component interface
- ConcreteDecorators add behavior before/after delegating to wrapped component
- Multiple decorators can be stacked/chained
Common Characteristics:
- Wrapping: Each decorator wraps another component
- Delegation: Decorators delegate to wrapped component
- Enhancement: Add behavior before/after delegation
- Transparency: Maintain the same interface as component
Class Diagram
Here's the UML class diagram showing the Decorator structure:
Beginner-Friendly Example
Let's start with a simple example: a coffee ordering system where you can add various condiments (decorators) to a base coffee.
from abc import ABC, abstractmethod
# Component
class Coffee(ABC):
"""Base component interface for coffee"""
@abstractmethod
def get_description(self) -> str:
pass
@abstractmethod
def get_cost(self) -> float:
pass
# ConcreteComponent
class SimpleCoffee(Coffee):
"""Basic coffee without any additions"""
def get_description(self) -> str:
return "Simple Coffee"
def get_cost(self) -> float:
return 2.0
# Decorator
class CoffeeDecorator(Coffee):
"""Base decorator class"""
def __init__(self, coffee: Coffee):
self._coffee = coffee
@abstractmethod
def get_description(self) -> str:
pass
@abstractmethod
def get_cost(self) -> float:
pass
# ConcreteDecorator
class Milk(CoffeeDecorator):
"""Adds milk to coffee"""
def get_description(self) -> str:
return f"{self._coffee.get_description()}, Milk"
def get_cost(self) -> float:
return self._coffee.get_cost() + 0.5
class Sugar(CoffeeDecorator):
"""Adds sugar to coffee"""
def get_description(self) -> str:
return f"{self._coffee.get_description()}, Sugar"
def get_cost(self) -> float:
return self._coffee.get_cost() + 0.2
class WhippedCream(CoffeeDecorator):
"""Adds whipped cream to coffee"""
def get_description(self) -> str:
return f"{self._coffee.get_description()}, Whipped Cream"
def get_cost(self) -> float:
return self._coffee.get_cost() + 0.7
# Usage
if __name__ == "__main__":
# Simple coffee
coffee = SimpleCoffee()
print(f"{coffee.get_description()}: ${coffee.get_cost():.2f}")
# Output: Simple Coffee: $2.00
# Coffee with milk
coffee_with_milk = Milk(SimpleCoffee())
print(f"{coffee_with_milk.get_description()}: ${coffee_with_milk.get_cost():.2f}")
# Output: Simple Coffee, Milk: $2.50
# Coffee with milk and sugar
coffee_deluxe = Sugar(Milk(SimpleCoffee()))
print(f"{coffee_deluxe.get_description()}: ${coffee_deluxe.get_cost():.2f}")
# Output: Simple Coffee, Milk, Sugar: $2.70
# Coffee with everything
coffee_special = WhippedCream(Sugar(Milk(SimpleCoffee())))
print(f"{coffee_special.get_description()}: ${coffee_special.get_cost():.2f}")
# Output: Simple Coffee, Milk, Sugar, Whipped Cream: $3.40
Production-Ready Example
Now let's look at a more realistic example: a data processing pipeline with various transformations (decorators) that can be applied to data streams.
š Python
from abc import ABC, abstractmethod
from typing import Any, Dict, List
from datetime import datetime
import json
import hashlib
import zlib
# Component
class DataProcessor(ABC):
"""Base component for data processing"""
@abstractmethod
def process(self, data: Any) -> Any:
"""Process the data"""
pass
@abstractmethod
def get_metadata(self) -> Dict[str, Any]:
"""Get processing metadata"""
pass
# ConcreteComponent
class BaseDataProcessor(DataProcessor):
"""Basic data processor without any transformations"""
def __init__(self):
self.processed_count = 0
self.start_time = datetime.now()
def process(self, data: Any) -> Any:
"""Pass through data unchanged"""
self.processed_count += 1
return data
def get_metadata(self) -> Dict[str, Any]:
return {
"processor": "BaseDataProcessor",
"processed_count": self.processed_count,
"runtime_seconds": (datetime.now() - self.start_time).total_seconds()
}
# Decorator
class DataProcessorDecorator(DataProcessor):
"""Base decorator for data processors"""
def __init__(self, processor: DataProcessor):
self._processor = processor
def process(self, data: Any) -> Any:
return self._processor.process(data)
def get_metadata(self) -> Dict[str, Any]:
return self._processor.get_metadata()
# ConcreteDecorators
class EncryptionDecorator(DataProcessorDecorator):
"""Encrypts data (simplified encryption for demo)"""
def __init__(self, processor: DataProcessor, encryption_key: str):
super().__init__(processor)
self.encryption_key = encryption_key
self.encrypted_count = 0
def process(self, data: Any) -> Any:
# Process data through wrapped processor first
processed_data = super().process(data)
# Apply encryption
if isinstance(processed_data, str):
# Simple XOR encryption for demo
encrypted = ''.join(
chr(ord(c) ^ ord(self.encryption_key[i % len(self.encryption_key)]))
for i, c in enumerate(processed_data)
)
self.encrypted_count += 1
return encrypted
return processed_data
def get_metadata(self) -> Dict[str, Any]:
metadata = super().get_metadata()
metadata.update({
"encryption": "enabled",
"encryption_algorithm": "XOR",
"encrypted_items": self.encrypted_count
})
return metadata
class CompressionDecorator(DataProcessorDecorator):
"""Compresses data"""
def __init__(self, processor: DataProcessor, compression_level: int = 6):
super().__init__(processor)
self.compression_level = compression_level
self.compressed_count = 0
self.original_size = 0
self.compressed_size = 0
def process(self, data: Any) -> Any:
# Process data through wrapped processor first
processed_data = super().process(data)
# Apply compression
if isinstance(processed_data, (str, bytes)):
data_bytes = processed_data.encode() if isinstance(processed_data, str) else processed_data
self.original_size += len(data_bytes)
compressed = zlib.compress(data_bytes, self.compression_level)
self.compressed_size += len(compressed)
self.compressed_count += 1
return compressed
return processed_data
def get_metadata(self) -> Dict[str, Any]:
metadata = super().get_metadata()
compression_ratio = (
(1 - self.compressed_size / self.original_size) * 100
if self.original_size > 0 else 0
)
metadata.update({
"compression": "enabled",
"compression_level": self.compression_level,
"compressed_items": self.compressed_count,
"original_size_bytes": self.original_size,
"compressed_size_bytes": self.compressed_size,
"compression_ratio_percent": f"{compression_ratio:.2f}"
})
return metadata
class ValidationDecorator(DataProcessorDecorator):
"""Validates data before processing"""
def __init__(self, processor: DataProcessor, required_fields: List[str]):
super().__init__(processor)
self.required_fields = required_fields
self.validated_count = 0
self.failed_count = 0
def process(self, data: Any) -> Any:
# Validate before processing
if isinstance(data, dict):
missing_fields = [field for field in self.required_fields if field not in data]
if missing_fields:
self.failed_count += 1
raise ValueError(f"Missing required fields: {missing_fields}")
self.validated_count += 1
# Process data through wrapped processor
return super().process(data)
def get_metadata(self) -> Dict[str, Any]:
metadata = super().get_metadata()
metadata.update({
"validation": "enabled",
"required_fields": self.required_fields,
"validated_items": self.validated_count,
"validation_failures": self.failed_count
})
return metadata
class LoggingDecorator(DataProcessorDecorator):
"""Logs all processing operations"""
def __init__(self, processor: DataProcessor, log_level: str = "INFO"):
super().__init__(processor)
self.log_level = log_level
self.logs: List[Dict[str, Any]] = []
def process(self, data: Any) -> Any:
timestamp = datetime.now().isoformat()
# Log before processing
self.logs.append({
"timestamp": timestamp,
"level": self.log_level,
"event": "processing_started",
"data_type": type(data).__name__,
"data_size": len(str(data))
})
try:
# Process data through wrapped processor
result = super().process(data)
# Log success
self.logs.append({
"timestamp": datetime.now().isoformat(),
"level": self.log_level,
"event": "processing_completed",
"result_type": type(result).__name__
})
return result
except Exception as e:
# Log error
self.logs.append({
"timestamp": datetime.now().isoformat(),
"level": "ERROR",
"event": "processing_failed",
"error": str(e)
})
raise
def get_metadata(self) -> Dict[str, Any]:
metadata = super().get_metadata()
metadata.update({
"logging": "enabled",
"log_level": self.log_level,
"log_entries": len(self.logs),
"recent_logs": self.logs[-5:] # Last 5 log entries
})
return metadata
class HashingDecorator(DataProcessorDecorator):
"""Adds hash/checksum to data"""
def __init__(self, processor: DataProcessor, algorithm: str = "sha256"):
super().__init__(processor)
self.algorithm = algorithm
self.hashed_count = 0
def process(self, data: Any) -> Any:
# Process data through wrapped processor first
processed_data = super().process(data)
# Add hash
if isinstance(processed_data, (str, bytes)):
data_bytes = processed_data.encode() if isinstance(processed_data, str) else processed_data
hash_obj = hashlib.new(self.algorithm)
hash_obj.update(data_bytes)
self.hashed_count += 1
# Return tuple of (data, hash)
return (processed_data, hash_obj.hexdigest())
return processed_data
def get_metadata(self) -> Dict[str, Any]:
metadata = super().get_metadata()
metadata.update({
"hashing": "enabled",
"hash_algorithm": self.algorithm,
"hashed_items": self.hashed_count
})
return metadata
# Usage and Testing
if __name__ == "__main__":
print("=== Production Data Processing Pipeline ===\n")
# Example 1: Simple processing
print("--- Example 1: Base Processor ---")
processor = BaseDataProcessor()
result = processor.process("Hello, World!")
print(f"Result: {result}")
print(f"Metadata: {json.dumps(processor.get_metadata(), indent=2)}\n")
# Example 2: With encryption
print("--- Example 2: With Encryption ---")
processor = EncryptionDecorator(
BaseDataProcessor(),
encryption_key="secret123"
)
result = processor.process("Sensitive data")
print(f"Encrypted: {repr(result)}")
print(f"Metadata: {json.dumps(processor.get_metadata(), indent=2)}\n")
# Example 3: With compression
print("--- Example 3: With Compression ---")
processor = CompressionDecorator(
BaseDataProcessor(),
compression_level=9
)
result = processor.process("This is some data that will be compressed!")
print(f"Compressed: {repr(result[:50])}...")
print(f"Metadata: {json.dumps(processor.get_metadata(), indent=2)}\n")
# Example 4: Validation + Logging
print("--- Example 4: With Validation and Logging ---")
processor = LoggingDecorator(
ValidationDecorator(
BaseDataProcessor(),
required_fields=["name", "email"]
),
log_level="DEBUG"
)
# Valid data
valid_data = {"name": "John Doe", "email": "john@example.com", "age": 30}
result = processor.process(valid_data)
print(f"Valid data processed: {result}")
# Invalid data
try:
invalid_data = {"name": "Jane Doe"} # Missing email
processor.process(invalid_data)
except ValueError as e:
print(f"Validation error: {e}")
print(f"Metadata: {json.dumps(processor.get_metadata(), indent=2)}\n")
# Example 5: Full pipeline (multiple decorators)
print("--- Example 5: Full Pipeline ---")
processor = HashingDecorator(
CompressionDecorator(
EncryptionDecorator(
LoggingDecorator(
ValidationDecorator(
BaseDataProcessor(),
required_fields=["id", "content"]
),
log_level="INFO"
),
encryption_key="mykey123"
),
compression_level=6
),
algorithm="sha256"
)
# Process data through full pipeline
data = {
"id": "12345",
"content": "This is important data that needs to be validated, logged, encrypted, compressed, and hashed!"
}
result = processor.process(json.dumps(data))
print(f"Pipeline result type: {type(result)}")
print(f"Hash: {result[1] if isinstance(result, tuple) else 'N/A'}")
# Get comprehensive metadata
metadata = processor.get_metadata()
print(f"\nFull Pipeline Metadata:")
print(json.dumps(metadata, indent=2))
# Example 6: Demonstrating flexibility
print("\n--- Example 6: Different Decorator Combinations ---")
# Configuration 1: Security focused
security_processor = HashingDecorator(
EncryptionDecorator(
BaseDataProcessor(),
encryption_key="secure_key"
),
algorithm="sha512"
)
# Configuration 2: Performance focused
performance_processor = CompressionDecorator(
BaseDataProcessor(),
compression_level=1 # Fast compression
)
# Configuration 3: Debugging focused
debug_processor = LoggingDecorator(
ValidationDecorator(
BaseDataProcessor(),
required_fields=["debug_id"]
),
log_level="DEBUG"
)
print("Three different pipeline configurations created successfully!")
print("Each can be swapped at runtime based on requirements.")
This production example demonstrates:
- Multiple decorators that can be combined in any order
- Real-world transformations (encryption, compression, validation, logging, hashing)
- Metadata tracking across the decorator chain
- Error handling and validation
- Flexible pipeline configuration
- Composable behavior that can be changed at runtime
Real-World Use Cases
The Decorator Pattern is particularly useful in these scenarios:
1. I/O Streams (Java/C# style)
Layering capabilities onto data streams:
# Component
class DataStream:
def read(self):
pass
def write(self, data):
pass
# ConcreteComponent
class FileStream(DataStream):
def __init__(self, filename):
self.file = open(filename, 'r+b')
def read(self):
return self.file.read()
def write(self, data):
self.file.write(data)
# Decorators
class BufferedStream(DataStream):
def __init__(self, stream):
self.stream = stream
self.buffer = []
def write(self, data):
self.buffer.append(data)
if len(self.buffer) >= 10:
self.flush()
def flush(self):
for data in self.buffer:
self.stream.write(data)
self.buffer.clear()
class CompressedStream(DataStream):
def __init__(self, stream):
self.stream = stream
def write(self, data):
compressed = compress(data)
self.stream.write(compressed)
# Usage
stream = CompressedStream(
BufferedStream(
FileStream("data.bin")
)
)
stream.write(b"data")
2. Web Request/Response Middleware
HTTP middleware pipeline:
class HTTPHandler:
def handle(self, request):
pass
class BaseHandler(HTTPHandler):
def handle(self, request):
return f"Handling: {request}"
class AuthenticationMiddleware(HTTPHandler):
def __init__(self, handler):
self.handler = handler
def handle(self, request):
if not self.authenticate(request):
return "Unauthorized"
return self.handler.handle(request)
def authenticate(self, request):
return "auth_token" in request
class LoggingMiddleware(HTTPHandler):
def __init__(self, handler):
self.handler = handler
def handle(self, request):
print(f"Request: {request}")
response = self.handler.handle(request)
print(f"Response: {response}")
return response
# Usage
handler = LoggingMiddleware(
AuthenticationMiddleware(
BaseHandler()
)
)
handler.handle({"auth_token": "123", "path": "/api/users"})
3. UI Component Enhancement
Adding visual features to UI components:
class UIComponent:
def render(self):
pass
class TextBox(UIComponent):
def __init__(self, text):
self.text = text
def render(self):
return f"[{self.text}]"
class BorderDecorator(UIComponent):
def __init__(self, component):
self.component = component
def render(self):
content = self.component.render()
return f"+{'='*len(content)}+\n|{content}|\n+{'='*len(content)}+"
class ScrollbarDecorator(UIComponent):
def __init__(self, component):
self.component = component
def render(self):
content = self.component.render()
return f"{content} [ā²ā¼]"
# Usage
textbox = BorderDecorator(
ScrollbarDecorator(
TextBox("Hello")
)
)
print(textbox.render())
4. Text Formatting
Layering text transformations:
class Text:
def render(self):
pass
class PlainText(Text):
def __init__(self, content):
self.content = content
def render(self):
return self.content
class BoldDecorator(Text):
def __init__(self, text):
self.text = text
def render(self):
return f"<b>{self.text.render()}</b>"
class ItalicDecorator(Text):
def __init__(self, text):
self.text = text
def render(self):
return f"<i>{self.text.render()}</i>"
class UnderlineDecorator(Text):
def __init__(self, text):
self.text = text
def render(self):
return f"<u>{self.text.render()}</u>"
# Usage
text = UnderlineDecorator(
ItalicDecorator(
BoldDecorator(
PlainText("Hello World")
)
)
)
print(text.render()) # <u><i><b>Hello World</b></i></u>
5. Caching Layer
Adding caching to expensive operations:
class DataSource:
def fetch(self, key):
pass
class Database(DataSource):
def fetch(self, key):
# Expensive database query
return f"Data for {key} from DB"
class CacheDecorator(DataSource):
def __init__(self, source):
self.source = source
self.cache = {}
def fetch(self, key):
if key not in self.cache:
self.cache[key] = self.source.fetch(key)
return self.cache[key]
class LoggingDecorator(DataSource):
def __init__(self, source):
self.source = source
def fetch(self, key):
print(f"Fetching {key}")
result = self.source.fetch(key)
print(f"Fetched: {result}")
return result
# Usage
source = LoggingDecorator(
CacheDecorator(
Database()
)
)
source.fetch("user:123") # DB hit + log
source.fetch("user:123") # Cache hit + log
Language-Specific Mistakes and Anti-Patterns
ā Anti-Pattern: Not Calling Super in Decorator
# BAD: Breaks the decorator chain
class BadDecorator(Decorator):
def operation(self):
# Forgot to call wrapped component!
return "only this decorator's behavior"
ā Solution: Always Delegate
# GOOD: Properly delegates to wrapped component
class GoodDecorator(Decorator):
def operation(self):
result = self._component.operation() # Delegate first
# Add decorator behavior
return f"decorated({result})"
ā Anti-Pattern: Mutable Default Arguments
# BAD: Shared mutable state across instances
class Decorator:
def __init__(self, component, cache={}): # Dangerous!
self.component = component
self.cache = cache
ā Solution: Use None and Create New
# GOOD: Each instance gets own cache
class Decorator:
def __init__(self, component, cache=None):
self.component = component
self.cache = cache if cache is not None else {}
Frequently Asked Questions
Q1: When should I use Decorator vs Inheritance?
Use Decorator when:
- ā You need to add responsibilities dynamically at runtime
- ā You want to avoid class explosion from subclass combinations
- ā Responsibilities can be combined in different ways
- ā You want to add/remove features without affecting other objects
Use Inheritance when:
- ā The relationship is truly "is-a"
- ā Behavior is static and known at compile time
- ā You need to override methods with different implementations
- ā You have a simple, stable hierarchy
Example:
# Decorator: Dynamic, composable features
coffee = WhippedCream(Sugar(Milk(SimpleCoffee()))) # Runtime composition
# Inheritance: Static hierarchy
class CoffeeWithMilkAndSugar(Coffee): # Compile-time, fixed
pass
Problem with inheritance for features:
# Class explosion!
class Coffee
class CoffeeWithMilk(Coffee)
class CoffeeWithSugar(Coffee)
class CoffeeWithMilkAndSugar(Coffee)
class CoffeeWithMilkSugarAndCream(Coffee)
# ... 2^n classes for n features!
Q2: How many decorators can I stack?
Technically unlimited, but practical considerations:
Performance impact:
# Each decorator adds one method call
result = d10(d9(d8(d7(d6(d5(d4(d3(d2(d1(base))))))))).operation()
# 11 method calls for one operation
Practical guidelines:
- 1-3 decorators: Minimal overhead, very readable
- 4-7 decorators: Acceptable for complex scenarios
- 8+ decorators: Consider if design can be simplified
Solutions for many decorators:
# Use a builder or factory
class ProcessorBuilder:
def __init__(self):
self.processor = BaseProcessor()
def with_encryption(self, key):
self.processor = EncryptionDecorator(self.processor, key)
return self
def with_compression(self, level=6):
self.processor = CompressionDecorator(self.processor, level)
return self
def build(self):
return self.processor
# Usage
processor = (ProcessorBuilder()
.with_encryption("key")
.with_compression(9)
.build())
Q3: Should decorators modify or replace the result?
Both are valid depending on your needs:
Modifying (wrapping) the result:
class BoldDecorator:
def render(self):
result = self.component.render()
return f"<b>{result}</b>" # Wraps/modifies result
Replacing the result:
class CacheDecorator:
def fetch(self, key):
if key in self.cache:
return self.cache[key] # Replaces with cached result
return self.component.fetch(key)
Adding to the result:
class TimestampDecorator:
def process(self, data):
result = self.component.process(data)
result['timestamp'] = datetime.now() # Adds to result
return result
Q4: Can decorators change the interface?
Generally no - that violates the Decorator pattern:
ā Wrong: Changing interface
class Component:
def operation(self) -> str:
pass
class BadDecorator(Component):
def operation(self) -> int: # Changed return type!
return 42
def new_method(self): # Added new method
pass # Clients can't call this without knowing it's BadDecorator
ā Right: Maintaining interface
class GoodDecorator(Component):
def operation(self) -> str: # Same interface
result = self.component.operation()
return f"decorated: {result}"
If you need to change the interface, you probably want:
Q5: How do I access the original component?
Usually you shouldn't - that defeats the purpose of decoration:
ā Anti-pattern: Bypassing decorators
coffee = Sugar(Milk(SimpleCoffee()))
# BAD: Trying to access original
original = coffee.component.component # Fragile!
ā Better: Design for transparency
# Client doesn't need to know about layers
coffee.get_cost() # Works through all decorators
coffee.get_description() # Works through all decorators
If you really need access, add a method:
class Decorator:
def get_innermost_component(self):
if isinstance(self.component, Decorator):
return self.component.get_innermost_component()
return self.component
Q6: How do I test decorated objects?
Test at multiple levels:
Unit test individual decorators:
def test_milk_decorator():
base = SimpleCoffee()
decorated = Milk(base)
assert decorated.get_cost() == base.get_cost() + 0.5
assert "Milk" in decorated.get_description()
Integration test decorator chains:
def test_multiple_decorators():
coffee = Sugar(Milk(SimpleCoffee()))
assert coffee.get_cost() == 2.7
assert coffee.get_description() == "Simple Coffee, Milk, Sugar"
Use mock objects:
class MockComponent:
def __init__(self):
self.operation_called = False
def operation(self):
self.operation_called = True
return "mock"
def test_decorator_delegates():
mock = MockComponent()
decorator = MyDecorator(mock)
decorator.operation()
assert mock.operation_called # Verify delegation
Q7: Can I remove decorators at runtime?
Not directly - decorators are immutable once constructed:
ā Can't do this:
coffee = WhippedCream(Sugar(Milk(SimpleCoffee())))
# Can't "unwrap" Sugar from the middle
ā Solutions:
Rebuild without unwanted decorator:
# Keep reference to components
base = SimpleCoffee()
with_milk = Milk(base)
with_sugar = Sugar(with_milk)
# Want to remove milk? Rebuild:
without_milk = Sugar(base)
Use a manager:
class DecoratorManager:
def __init__(self, base):
self.base = base
self.decorators = []
def add_decorator(self, decorator_class, *args):
self.decorators.append((decorator_class, args))
def remove_decorator(self, decorator_class):
self.decorators = [
(cls, args) for cls, args in self.decorators
if cls != decorator_class
]
def build(self):
result = self.base
for decorator_class, args in self.decorators:
result = decorator_class(result, *args)
return result
Q8: How does Decorator differ from Chain of Responsibility?
Key differences:
| Aspect | Decorator | Chain of Responsibility |
|---|---|---|
| Purpose | Add behavior | Pass request along chain |
| All execute | Yes, all decorators run | No, one handler processes |
| Return | Wraps/modifies result | Returns when handled |
| Order | Matters (nesting order) | Matters (first match) |
| Structure | Each wraps previous | Each points to next |
Example:
# Decorator: ALL execute
result = d3(d2(d1(base))).operation()
# Execution: base ā d1 ā d2 ā d3 (all run)
# Chain: FIRST handler processes
result = h1.handle(request) # h1 ā h2 ā h3
# Execution: h1 tries, if can't handle ā h2 tries, if can't ā h3 tries
# (stops when one handles it)
Q9: Should decorator constructors do work?
Generally no - keep constructors simple:
ā Anti-pattern: Heavy constructor
class CacheDecorator:
def __init__(self, component):
self.component = component
self.cache = self.load_cache_from_disk() # Slow I/O!
self.initialize_connection() # Network call!
ā Better: Lazy initialization
class CacheDecorator:
def __init__(self, component):
self.component = component
self._cache = None
@property
def cache(self):
if self._cache is None:
self._cache = self.load_cache_from_disk()
return self._cache
Or inject dependencies:
class CacheDecorator:
def __init__(self, component, cache):
self.component = component
self.cache = cache # Injected, already initialized
Q10: How do I serialize/deserialize decorated objects?
Serialization is challenging because of wrapping:
Problem:
coffee = WhippedCream(Sugar(Milk(SimpleCoffee())))
# How to serialize this structure?
Solution 1: Store configuration
class CoffeeBuilder:
def __init__(self):
self.base = "SimpleCoffee"
self.additions = []
def add(self, decorator_name, **kwargs):
self.additions.append((decorator_name, kwargs))
return self
def to_dict(self):
return {
"base": self.base,
"additions": self.additions
}
@staticmethod
def from_dict(data):
builder = CoffeeBuilder()
builder.base = data["base"]
builder.additions = data["additions"]
return builder
def build(self):
coffee = SimpleCoffee()
for decorator_name, kwargs in self.additions:
decorator_class = globals()[decorator_name]
coffee = decorator_class(coffee, **kwargs)
return coffee
# Usage
builder = CoffeeBuilder()
builder.add("Milk").add("Sugar").add("WhippedCream")
# Serialize
config = builder.to_dict()
json_str = json.dumps(config)
# Deserialize
restored_builder = CoffeeBuilder.from_dict(json.loads(json_str))
coffee = restored_builder.build()
Solution 2: Custom serialization
class SerializableDecorator:
def to_dict(self):
return {
"type": self.__class__.__name__,
"component": self.component.to_dict() if hasattr(self.component, 'to_dict') else None
}
@staticmethod
def from_dict(data):
decorator_class = globals()[data["type"]]
component = SerializableDecorator.from_dict(data["component"]) if data["component"] else SimpleCoffee()
return decorator_class(component)
Key Takeaways
Let's wrap up what we've learned about the Decorator Pattern across multiple programming languages:
šÆ Core Concept: Decorator allows you to attach additional responsibilities to an object dynamically by wrapping it in decorator objects. It provides a flexible alternative to subclassing for extending functionality.
š Key Characteristics:
- Wrapping: Each decorator wraps another component (or decorator)
- Same Interface: Decorators implement the same interface as the component
- Delegation: Decorators delegate to wrapped component
- Composable: Multiple decorators can be stacked in any order
- Runtime Configuration: Features can be added/removed at runtime
š Language-Specific Considerations:
- Python: Use ABC for interfaces; careful with mutable defaults
- Java: Make wrapped component final; implement all interface methods
- C#: Validate null; use readonly for component reference
- JavaScript/TypeScript: Use private fields (TS); maintain type safety
- Kotlin: Use
bydelegation for automatic forwarding - Go: Use interfaces; validate nil; prefer composition
- Rust: Use trait objects; handle ownership with Box<dyn Trait>
- Swift: Use protocols; consider value vs reference semantics
- Dart: Use abstract classes; leverage null safety
- PHP: Use type hints; consider readonly properties (PHP 8.1+)
š When to Use Decorator:
- I/O streams (buffering, compression, encryption)
- UI component enhancement (borders, scrollbars, shadows)
- Middleware pipelines (logging, authentication, caching)
- Text formatting (bold, italic, underline)
- Data processing (validation, transformation, filtering)
- Feature toggling (enable/disable features at runtime)
- Cross-cutting concerns (logging, monitoring, security)
ā ļø When NOT to Use Decorator:
- Simple scenarios where subclassing suffices
- When you need to change the interface (use Adapter)
- When you need to choose one algorithm (use Strategy)
- When decorator order creates tight coupling
- When you have too many small decorators (consider Composite)
- When performance overhead of wrapping is significant
š ļø Best Practices Across All Languages:
- Maintain Interface: Decorators must implement component interface
- Always Delegate: Call wrapped component's methods
- Validate Constructor Args: Check for null/nil wrapped component
- Make Component Immutable: Use final/readonly for wrapped component
- Keep Decorators Simple: Each decorator has single responsibility
- Consider Order: Document if decorator order matters
- Use Factories/Builders: For complex decorator chains
- Test Individually: Unit test each decorator separately
āļø Decorator vs. Related Patterns:
- vs. Composite: Decorator adds responsibilities; Composite structures trees
- vs. Adapter: Decorator keeps interface; Adapter changes it
- vs. Proxy: Decorator adds behavior; Proxy controls access
- vs. Strategy: Decorator wraps; Strategy replaces algorithm
- vs. Chain of Responsibility: All decorators execute; Chain stops at first handler
š” Remember:
- Decorator is about adding responsibilities, not changing interface
- Wrapping is the key mechanism
- Order of decorators can matter (encryption before compression is different from compression before encryption)
- Decorators are composable - mix and match at runtime
- Each decorator should follow Single Responsibility Principle
- Too many decorators can indicate design problems
š Common Implementation Patterns:
- Abstract Decorator Base: Implements interface, delegates by default
- Transparent Decorator: Clients don't know they're using decorators
- Builder Pattern: Helps construct complex decorator chains
- Factory Pattern: Creates common decorator combinations
- Fluent Interface: Chain decorator creation with builder
- Configuration-Based: Build decorators from config/data
Design Decisions to Make:
- Should decorators modify, replace, or enhance results?
- Should decorators be stateless or stateful?
- How many decorators is too many?
- Should decorator order be enforced?
- How to handle errors in decorator chain?
- Should decorators be serializable?
The Decorator Pattern is one of the most versatile structural patterns for adding behavior dynamically. By maintaining the same interface while wrapping objects with new functionality, it provides enormous flexibility without the complexity of subclass proliferation. While it adds some runtime overhead and complexity, the benefits in terms of composability and maintainability make it invaluable for systems that need flexible, configurable behavior.
Want to dive deeper into design patterns? Check out our comprehensive guides on:
- Proxy Pattern
- Adapter Pattern
- Composite Pattern
- Bridge Pattern
- Facade Pattern Each guide includes examples in all 11 programming languages with language-specific best practices.