Decorator Pattern — Add Behavior Dynamically
What You'll Learn
You will learn how the Decorator pattern wraps objects with new behaviour in a composable, stackable fashion, offering a flexible alternative to subclassing. You will understand when to reach for Decorator versus static inheritance.
Why It Matters
Subclassing leads to an explosion of classes — one for every combination of features. A coffee shop with three beverage types and four condiments would need 12 classes. With Decorator, you compose behaviour at runtime, adding only one class per condiment. The combinatorial problem grows exponentially: ten features would require hundreds of subclasses to cover every combination. Decorator reduces this to ten classes, one per feature, composed as needed.
Real-World Use
Stream APIs in programming languages chain filters, buffers, and compression layers around raw I/O streams — BufferedInputStream(GZIPInputStream(FileInputStream("file.gz"))). DodaTech's middleware pipeline uses decorators to layer authentication, logging, rate-limiting, and Caching around HTTP handlers without touching the core handler. Adding a new middleware simply means writing one class and inserting it into the pipeline — no modifications to existing handlers.
The Pattern
Component defines the interface. ConcreteComponent is the core object. Decorator maintains a reference to a Component and adds its own behaviour before or after delegating.
from abc import ABC, abstractmethod
class Beverage(ABC):
@abstractmethod
def cost(self) -> float:
pass
@abstractmethod
def description(self) -> str:
pass
class Espresso(Beverage):
def cost(self) -> float:
return 2.50
def description(self) -> str:
return "Espresso"
class CondimentDecorator(Beverage):
def __init__(self, beverage: Beverage):
self._beverage = beverage
class Milk(CondimentDecorator):
def cost(self) -> float:
return self._beverage.cost() + 0.50
def description(self) -> str:
return self._beverage.description() + ", Milk"
class Whip(CondimentDecorator):
def cost(self) -> float:
return self._beverage.cost() + 0.30
def description(self) -> str:
return self._beverage.description() + ", Whip"
class Caramel(CondimentDecorator):
def cost(self) -> float:
return self._beverage.cost() + 0.75
def description(self) -> str:
return self._beverage.description() + ", Caramel"
drink = Espresso()
drink = Milk(drink)
drink = Whip(drink)
drink = Caramel(drink)
print(f"{drink.description()} — ${drink.cost():.2f}")
Espresso, Milk, Whip, Caramel — $4.05
Structure
classDiagram
class Component {
<>
+operation()
}
class ConcreteComponent {
+operation()
}
class Decorator {
#component: Component
+operation()
}
class ConcreteDecoratorA {
+operation()
}
class ConcreteDecoratorB {
+operation()
}
ConcreteComponent ..|> Component
Decorator ..|> Component
ConcreteDecoratorA --|> Decorator
ConcreteDecoratorB --|> Decorator
Decorator --> Component : delegates
Real-World Usage
- Python
@decoratorsyntax — functions can be wrapped with behaviour like@staticmethod,@lru_cache, or custom decorators. - Java I/O Streams —
new BufferedInputStream(new GZIPInputStream(new FileInputStream("file.gz")))layers behaviour. - .NET Middleware Pipeline — ASP.NET Core uses
app.Use()to stack middleware components around the request handler. - Ruby on Rails
before_action— interceptors that add cross-cutting behaviour to controller methods.
Related Patterns
- Adapter changes the interface; Decorator preserves it while adding behaviour.
- Composite aggregates children; Decorator wraps a single component.
- Strategy swaps algorithms; Decorator layers responsibilities.
- Proxy controls access; Decorator adds responsibility.
Pros and Cons
| Pros | Cons |
|---|---|
| More flexible than static inheritance | Many small classes increase codebase size |
| Behaviour can be composed at runtime | Order of wrapping matters and can cause subtle bugs |
| Follows the Open/Closed Principle | Debugging a heavily decorated object is harder |
| Single Responsibility Principle — each decorator does one thing | Decorator and component types must match exactly |
Notice in the code that each decorator both wraps and conforms to the Beverage interface. This is the defining characteristic of the Decorator pattern — the wrapper is transparent to the client. The drink variable, after being wrapped three times, is still a Beverage and can be used anywhere a Beverage is expected.
Practice Questions
- How would you implement a logging decorator that records method entry and exit with timestamps?
- Compare Decorator with subclassing — when does the tipping point favour one over the other? Consider a UI component with optional borders, scrollbars, and shadows.
- Implement a decorator that caches the results of a computationally expensive operation, invalidating the cache when the underlying component changes.
- What challenges arise when a decorator needs to access the concrete component's specialised methods that aren't part of the common interface?
Challenge
Implement a decorator that measures and reports execution time for any operation. Chain it with existing decorators and verify that the timing wraps the entire decorated call stack, not just the innermost operation.
Real-World Task
Look at your application's HTTP middleware stack. Map each middleware to a Decorator class and use DodaTech's middleware profiler to measure the performance contribution of each layer in the pipeline.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro