Decorator Pattern: Add Behavior Dynamically
TL;DR
The Decorator Pattern lets you add new responsibilities to objects dynamically without modifying their code. Instead of using inheritance to extend functionality, you wrap objects in decorator classes that add behavior while maintaining the same interface. This provides a flexible alternative to subclassing for extending functionality.
Core Concept
What is the Decorator Pattern?
The Decorator Pattern is a structural design pattern that allows you to attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
Instead of creating a complex inheritance hierarchy with every possible combination of features, you create small decorator classes that each add one specific behavior. You can then wrap an object in multiple decorators, stacking behaviors like layers.
Why Use Decorators?
Imagine a coffee shop system. You have a base Coffee class, but customers can add milk, sugar, whipped cream, or caramel. Creating a subclass for every combination (CoffeeWithMilk, CoffeeWithMilkAndSugar, CoffeeWithMilkSugarAndWhippedCream) quickly becomes unmanageable — you’d need dozens of classes.
With decorators, you create one decorator for each add-on. You can then combine them at runtime: wrap a Coffee in a MilkDecorator, wrap that in a SugarDecorator, and so on. Each decorator adds its cost and description to the wrapped object.
Key Components
- Component Interface: Defines the interface that both concrete components and decorators implement
- Concrete Component: The base object that can be decorated
- Decorator Base Class: Implements the component interface and contains a reference to a component
- Concrete Decorators: Extend the decorator base class and add specific responsibilities
How It Works
Each decorator wraps another component (which could be the base object or another decorator). When a method is called on a decorator, it typically:
- Calls the same method on the wrapped component
- Adds its own behavior before or after that call
- Returns the combined result
This creates a chain of responsibility where each decorator adds its layer of functionality.
When to Use Decorators
- You need to add responsibilities to individual objects, not entire classes
- You want to add or remove responsibilities dynamically at runtime
- Extension by subclassing is impractical (too many combinations)
- You want to follow the Single Responsibility Principle (each decorator does one thing)
Visual Guide
Decorator Pattern Structure
classDiagram
class Component {
<<interface>>
+operation()
}
class ConcreteComponent {
+operation()
}
class Decorator {
-component: Component
+operation()
}
class ConcreteDecoratorA {
+operation()
+addedBehavior()
}
class ConcreteDecoratorB {
+operation()
}
Component <|.. ConcreteComponent
Component <|.. Decorator
Component --o Decorator
Decorator <|-- ConcreteDecoratorA
Decorator <|-- ConcreteDecoratorB
The Decorator wraps a Component and implements the same interface. Concrete decorators extend the base Decorator to add specific behaviors.
Coffee Shop Example Flow
graph LR
A[SimpleCoffee] --> B[MilkDecorator wraps SimpleCoffee]
B --> C[SugarDecorator wraps MilkDecorator]
C --> D[WhippedCreamDecorator wraps SugarDecorator]
D --> E[Final: Coffee + Milk + Sugar + Whipped Cream]
Each decorator wraps the previous component, creating a chain that combines all behaviors.
Examples
Example 1: Coffee Shop System
Let’s build a coffee ordering system where customers can customize their drinks with various add-ons.
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
def description(self) -> str:
return "Simple coffee"
# Decorator Base Class
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()
# 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"
class WhippedCreamDecorator(CoffeeDecorator):
def cost(self) -> float:
return self._coffee.cost() + 0.7
def description(self) -> str:
return self._coffee.description() + ", whipped cream"
# Usage
if __name__ == "__main__":
# Simple coffee
coffee = SimpleCoffee()
print(f"{coffee.description()}: ${coffee.cost():.2f}")
# Output: Simple coffee: $2.00
# Coffee with milk
coffee_with_milk = MilkDecorator(SimpleCoffee())
print(f"{coffee_with_milk.description()}: ${coffee_with_milk.cost():.2f}")
# Output: Simple coffee, milk: $2.50
# Coffee with milk and sugar
coffee_deluxe = SugarDecorator(MilkDecorator(SimpleCoffee()))
print(f"{coffee_deluxe.description()}: ${coffee_deluxe.cost():.2f}")
# Output: Simple coffee, milk, sugar: $2.70
# Full customization
fancy_coffee = WhippedCreamDecorator(
SugarDecorator(
MilkDecorator(SimpleCoffee())
)
)
print(f"{fancy_coffee.description()}: ${fancy_coffee.cost():.2f}")
# Output: Simple coffee, milk, sugar, whipped cream: $3.40
Key Points:
- Each decorator wraps a
Coffeeobject and adds its own cost and description - You can stack decorators in any order and combination
- The client code doesn’t need to know about the decoration — it just calls
cost()anddescription() - Adding a new add-on requires only one new decorator class
Try it yourself: Add a CaramelDecorator that costs $0.6 and a VanillaDecorator that costs $0.4. Create a coffee with all possible add-ons.
Example 2: Text Formatting System
Let’s create a system that can apply multiple formatting options to text dynamically.
from abc import ABC, abstractmethod
# Component Interface
class TextComponent(ABC):
@abstractmethod
def render(self) -> str:
pass
# Concrete Component
class PlainText(TextComponent):
def __init__(self, text: str):
self._text = text
def render(self) -> str:
return self._text
# Decorator Base Class
class TextDecorator(TextComponent):
def __init__(self, component: TextComponent):
self._component = component
def render(self) -> str:
return self._component.render()
# Concrete Decorators
class BoldDecorator(TextDecorator):
def render(self) -> str:
return f"<b>{self._component.render()}</b>"
class ItalicDecorator(TextDecorator):
def render(self) -> str:
return f"<i>{self._component.render()}</i>"
class UnderlineDecorator(TextDecorator):
def render(self) -> str:
return f"<u>{self._component.render()}</u>"
class UpperCaseDecorator(TextDecorator):
def render(self) -> str:
return self._component.render().upper()
# Usage
if __name__ == "__main__":
# Plain text
text = PlainText("Hello, World!")
print(text.render())
# Output: Hello, World!
# Bold text
bold_text = BoldDecorator(PlainText("Hello, World!"))
print(bold_text.render())
# Output: <b>Hello, World!</b>
# Bold and italic
styled_text = ItalicDecorator(BoldDecorator(PlainText("Hello, World!")))
print(styled_text.render())
# Output: <i><b>Hello, World!</b></i>
# All formatting options
fancy_text = UnderlineDecorator(
ItalicDecorator(
BoldDecorator(
UpperCaseDecorator(
PlainText("Hello, World!")
)
)
)
)
print(fancy_text.render())
# Output: <u><i><b>HELLO, WORLD!</b></i></u>
# Order matters!
different_order = UpperCaseDecorator(
BoldDecorator(
PlainText("Hello, World!")
)
)
print(different_order.render())
# Output: <B>HELLO, WORLD!</B> (tags are also uppercased)
Key Points:
- The order of decorators matters — different orders produce different results
- Each decorator is independent and can be combined with any other
- The base
PlainTextobject is never modified — decorators wrap it - This is more flexible than creating classes like
BoldItalicText,BoldUnderlineText, etc.
Java/C++ Notes:
- In Java, you’d use interfaces instead of abstract base classes:
interface Coffee { double cost(); String description(); } - In C++, you’d use pure virtual functions:
virtual double cost() = 0; - Java and C++ require explicit memory management for wrapped objects (especially C++ with pointers/smart pointers)
Try it yourself: Create a ColorDecorator that takes a color parameter and wraps text in <span style="color: {color}">. Combine it with other decorators.
Example 3: Data Stream Processing
A practical example showing how decorators can add compression and encryption to data streams.
from abc import ABC, abstractmethod
import base64
import zlib
# Component Interface
class DataSource(ABC):
@abstractmethod
def write(self, data: str) -> None:
pass
@abstractmethod
def read(self) -> str:
pass
# Concrete Component
class FileDataSource(DataSource):
def __init__(self, filename: str):
self._filename = filename
self._data = ""
def write(self, data: str) -> None:
self._data = data
print(f"Writing to {self._filename}: {data[:50]}...")
def read(self) -> str:
print(f"Reading from {self._filename}")
return self._data
# Decorator Base Class
class DataSourceDecorator(DataSource):
def __init__(self, source: DataSource):
self._source = source
def write(self, data: str) -> None:
self._source.write(data)
def read(self) -> str:
return self._source.read()
# Concrete Decorators
class CompressionDecorator(DataSourceDecorator):
def write(self, data: str) -> None:
compressed = zlib.compress(data.encode())
print(f"Compressing data: {len(data)} -> {len(compressed)} bytes")
# Convert to base64 for string representation
compressed_str = base64.b64encode(compressed).decode()
self._source.write(compressed_str)
def read(self) -> str:
compressed_str = self._source.read()
compressed = base64.b64decode(compressed_str.encode())
decompressed = zlib.decompress(compressed).decode()
print(f"Decompressing data: {len(compressed)} -> {len(decompressed)} bytes")
return decompressed
class EncryptionDecorator(DataSourceDecorator):
def __init__(self, source: DataSource, key: int = 13):
super().__init__(source)
self._key = key
def write(self, data: str) -> None:
encrypted = self._encrypt(data)
print(f"Encrypting data with key {self._key}")
self._source.write(encrypted)
def read(self) -> str:
encrypted = self._source.read()
decrypted = self._decrypt(encrypted)
print(f"Decrypting data with key {self._key}")
return decrypted
def _encrypt(self, data: str) -> str:
# Simple Caesar cipher for demonstration
return ''.join(chr((ord(char) + self._key) % 256) for char in data)
def _decrypt(self, data: str) -> str:
return ''.join(chr((ord(char) - self._key) % 256) for char in data)
# Usage
if __name__ == "__main__":
# Plain file writing
print("=== Plain File ===")
plain = FileDataSource("data.txt")
plain.write("Hello, World! This is a test message.")
print(f"Read: {plain.read()}")
print()
# With compression
print("=== With Compression ===")
compressed = CompressionDecorator(FileDataSource("data.txt"))
compressed.write("Hello, World! This is a test message.")
print(f"Read: {compressed.read()}")
print()
# With encryption and compression
print("=== With Encryption and Compression ===")
secure = EncryptionDecorator(
CompressionDecorator(
FileDataSource("data.txt")
)
)
secure.write("Hello, World! This is a test message.")
print(f"Read: {secure.read()}")
print()
# Output:
# === Plain File ===
# Writing to data.txt: Hello, World! This is a test message....
# Reading from data.txt
# Read: Hello, World! This is a test message.
#
# === With Compression ===
# Compressing data: 38 -> 46 bytes
# Writing to data.txt: eJzLSM3JyVcozy/KSVEEABKJBXw=...
# Reading from data.txt
# Decompressing data: 46 -> 38 bytes
# Read: Hello, World! This is a test message.
#
# === With Encryption and Compression ===
# Compressing data: 38 -> 46 bytes
# Encrypting data with key 13
# Writing to data.txt: rW{8Vy{rYVxrYV~rYV{rYV|rYV}...
# Reading from data.txt
# Decrypting data with key 13
# Decompressing data: 46 -> 38 bytes
# Read: Hello, World! This is a test message.
Key Points:
- Decorators can transform data as it passes through the chain
- The order matters: encrypt-then-compress vs compress-then-encrypt produces different results
- Each decorator is responsible for both encoding (write) and decoding (read)
- The base
FileDataSourcedoesn’t know about compression or encryption
Try it yourself: Add a LoggingDecorator that prints timestamps and operation details before delegating to the wrapped component.
Common Mistakes
1. Forgetting to Delegate to the Wrapped Component
Mistake: Implementing decorator methods without calling the wrapped component’s methods.
class BadDecorator(CoffeeDecorator):
def cost(self) -> float:
return 0.5 # Wrong! Doesn't include base cost
Why it’s wrong: The decorator replaces the base functionality instead of extending it. The whole point is to add to existing behavior.
Correct approach:
class GoodDecorator(CoffeeDecorator):
def cost(self) -> float:
return self._coffee.cost() + 0.5 # Adds to base cost
2. Creating Too Many Decorator Layers
Mistake: Wrapping objects in excessive layers of decorators, making code hard to read and debug.
# Hard to read and maintain
coffee = CaramelDecorator(
WhippedCreamDecorator(
SugarDecorator(
MilkDecorator(
VanillaDecorator(
SimpleCoffee()
)
)
)
)
)
Why it’s wrong: Deep nesting makes code unreadable and debugging difficult. If there’s an error, which decorator caused it?
Better approach: Use a builder pattern or factory to construct decorated objects, or consider if you really need that many decorators.
class CoffeeBuilder:
def __init__(self):
self._coffee = SimpleCoffee()
def add_milk(self):
self._coffee = MilkDecorator(self._coffee)
return self
def add_sugar(self):
self._coffee = SugarDecorator(self._coffee)
return self
def build(self):
return self._coffee
# Much more readable
coffee = CoffeeBuilder().add_milk().add_sugar().build()
3. Not Maintaining the Same Interface
Mistake: Adding methods to decorators that aren’t in the base component interface.
class BadMilkDecorator(CoffeeDecorator):
def cost(self) -> float:
return self._coffee.cost() + 0.5
def get_milk_type(self) -> str: # New method not in Coffee interface
return "whole milk"
Why it’s wrong: Clients can’t use the new method polymorphically. If you have a Coffee reference, you can’t call get_milk_type() without type checking.
When it’s acceptable: If you’re certain clients will know the concrete decorator type and need the extra functionality. But this breaks the pattern’s intent.
4. Using Decorators When Inheritance Would Be Simpler
Mistake: Applying the Decorator Pattern when you have a fixed, small number of variations that won’t change.
Example: If you only ever have “Regular Coffee” and “Decaf Coffee” with no customization, two simple subclasses are clearer than decorators.
Why it’s wrong: Overengineering. Decorators add complexity — use them when you need runtime flexibility and multiple combinations.
Rule of thumb: Use decorators when you have multiple orthogonal features that can be combined in many ways. Use inheritance when you have a few distinct, fixed types.
5. Incorrect Decorator Order Assumptions
Mistake: Assuming decorators are order-independent when they’re not.
# These produce different results!
text1 = UpperCaseDecorator(BoldDecorator(PlainText("hello")))
print(text1.render()) # <B>HELLO</B> (tags uppercased)
text2 = BoldDecorator(UpperCaseDecorator(PlainText("hello")))
print(text2.render()) # <b>HELLO</b> (tags not uppercased)
Why it matters: The order of decorators affects the final result. Always document the expected order or make decorators order-independent when possible.
Best practice: Write unit tests for different decorator orders to ensure they behave as expected.
Interview Tips
What Interviewers Look For
1. Can you explain when to use Decorator vs. Inheritance?
Be ready to articulate: “Use decorators when you need to add responsibilities dynamically at runtime, or when you have many possible combinations of features. Use inheritance when you have a fixed set of types known at compile time. Decorators favor composition over inheritance and follow the Open/Closed Principle.”
2. Implement a decorator system from scratch
Interviewers often ask you to design a system like the coffee shop example. Practice implementing:
- The component interface
- A concrete component
- The decorator base class
- At least two concrete decorators
- Client code that uses them
Time yourself — you should be able to write a basic decorator system in 10-15 minutes.
3. Identify real-world examples
Be prepared to discuss real-world uses:
- Java I/O streams:
BufferedReader(new FileReader("file.txt"))— each wrapper adds functionality - Python decorators: The
@syntax is related but not the same pattern (it’s more like function wrappers) - GUI components: Adding scrollbars, borders, or shadows to windows
- Middleware in web frameworks: Each middleware wraps the request/response handling
4. Discuss trade-offs
Interviewers want to know you understand the downsides:
- Complexity: More objects to manage, harder to debug
- Identity issues: A decorated object isn’t the same type as the base object (matters for
isinstancechecks) - Order dependency: Decorator order can affect behavior
- Performance: Extra method calls through the chain
Always mention: “I’d use decorators when flexibility is more important than simplicity.”
Common Interview Questions
Q: How is the Decorator Pattern different from the Proxy Pattern?
A: Both wrap an object, but their intent differs. A decorator adds responsibilities, while a proxy controls access. A proxy might add lazy loading, access control, or remote communication, but doesn’t fundamentally change what the object does. A decorator changes or enhances behavior.
Q: Can you have a decorator that wraps multiple components?
A: Typically no — decorators wrap a single component to maintain the same interface. If you need to combine multiple components, consider the Composite Pattern instead.
Q: How do you handle decorator removal at runtime?
A: This is tricky. You’d need to keep references to each layer and reconstruct the chain without the unwanted decorator. This is a sign that decorators might not be the best choice — consider the Strategy Pattern for swappable behavior.
Coding Exercise to Practice
Implement a notification system where you can send notifications via email, SMS, and Slack. Use decorators to add features like:
- Logging (records when notifications are sent)
- Retry logic (retries failed sends)
- Rate limiting (prevents sending too many notifications)
Make sure your solution allows combining these features in any order.
Red Flags to Avoid
- Don’t confuse Python’s
@decoratorsyntax with the Decorator Pattern — they’re related but not the same - Don’t say “decorators are always better than inheritance” — know when each is appropriate
- Don’t forget to implement the same interface in your decorators
- Don’t create decorators that depend on specific decorator order without documenting it
Key Takeaways
-
The Decorator Pattern adds responsibilities to objects dynamically by wrapping them in decorator classes that implement the same interface, providing a flexible alternative to subclassing.
-
Each decorator wraps a component and delegates to it while adding its own behavior before or after the delegation, allowing you to stack multiple decorators to combine behaviors.
-
Use decorators when you need runtime flexibility and have many possible combinations of features; use inheritance when you have a fixed, small set of types known at compile time.
-
Decorators follow the Open/Closed Principle — you can extend functionality without modifying existing code by adding new decorator classes.
-
Order matters in decorator chains — different stacking orders can produce different results, so document expected order or design decorators to be order-independent when possible.