Decorator
Add responsibilities to an object by wrapping it in another object with the same interface — stackable, and chosen at runtime.
Big idea
A Decorator wraps an object, offers the same interface, and adds something before or after delegating. Because a decorated object looks exactly like the original, decorators stack: logging around caching around retries around the real call — each one small, each one optional.
The problem it solves
A PriceService fetches prices over the network. Different callers want: logging; caching; retries on timeouts; timing metrics; sometimes all of them, sometimes one. Subclassing gives you LoggingPriceService, CachingPriceService, LoggingCachingPriceService, LoggingCachingRetryingPriceService… one class per combination, fixed at compile time, with the logging code copied into several of them.
Think of it like
Dressing for weather. You are the same person underneath; you add a jumper, then a raincoat, then a scarf. Each layer adds one thing, you can take any layer off, and the order matters a little (raincoat on the outside). Nobody manufactures a "person-with-jumper-and-raincoat".
Structure
In code
class FlakyPrices: # the real component
def __init__(self):
self.calls = 0
def price(self, symbol):
self.calls += 1
if self.calls == 1:
raise TimeoutError("slow network")
return {"ACME": 12.5, "GLOBEX": 40.0}[symbol]
class Decorator:
def __init__(self, inner):
self.inner = inner
def price(self, symbol):
return self.inner.price(symbol)
class Retrying(Decorator):
def price(self, symbol):
try:
return self.inner.price(symbol)
except TimeoutError:
print(" retrying", symbol)
return self.inner.price(symbol)
class Caching(Decorator):
def __init__(self, inner):
super().__init__(inner)
self.cache = {}
def price(self, symbol):
if symbol not in self.cache:
self.cache[symbol] = self.inner.price(symbol)
return self.cache[symbol]
class Logging(Decorator):
def price(self, symbol):
value = self.inner.price(symbol)
print(f" price({symbol}) -> {value}")
return value
source = FlakyPrices()
service = Logging(Caching(Retrying(source))) # composed at runtime
for s in ["ACME", "ACME", "GLOBEX"]:
service.price(s)
print("network calls:", source.calls)output
retrying ACME
price(ACME) -> 12.5
price(ACME) -> 12.5
price(GLOBEX) -> 40.0
network calls: 3Run and edit it in the app.
Read the stack from the outside in: log every request; serve from cache when possible; on a miss, retry once on timeout. Reorder the wrappers and you change behaviour — Caching(Logging(...)) would only log misses.
class Source:
def get(self, k):
return k.upper()
class Log:
def __init__(self, inner): self.inner = inner
def get(self, k):
print("log", k)
return self.inner.get(k)
class Cache:
def __init__(self, inner): self.inner, self.seen = inner, {}
def get(self, k):
if k not in self.seen:
self.seen[k] = self.inner.get(k)
return self.seen[k]
svc = Cache(Log(Source()))
for k in ["a", "a", "b", "a"]:
svc.get(k)
print("done")What does it print? Show the answer
log a
log b
doneRun and edit it in the app.
Watch it
Python's built-in decorators
Python's @decorator syntax applies the same idea to functions: @functools.lru_cache wraps a function with caching, keeping its call signature. The pattern and the syntax share a name because they share the idea — wrap, keep the interface, add behaviour.
Why prefer decorators over subclasses for optional add-ons like logging and caching?
When to use it — and when not to
Use it for optional, combinable behaviour around a stable interface: I/O streams (buffering, compression, encryption), HTTP middleware, caching, metrics, authorization checks. Skip it when the extra behaviour is always needed (just put it in the class) or when it needs to change the interface (that's an Adapter).
Pros and cons
| Pros | Cons |
|---|---|
| Behaviour added and removed at runtime | Many small objects; stack traces get deep |
| Each concern in one small class (Single Responsibility) | Order of wrapping matters and is easy to get wrong |
| No subclass explosion | Identity checks (is) see the wrapper, not the original |
How it relates
- Adapter changes the interface; Decorator keeps it; Proxy keeps it too but controls access rather than adding features.
- A decorator is a one-child Composite with a different purpose.
- Chain of Responsibility looks similar, but each handler may stop the request; decorators always pass it on.
- Strategy changes an object's insides; Decorator changes its outside.
A wrapper with the same interface delays creating an expensive object until first use. Decorator or Proxy?
Key terms
- Decorator — A wrapper with the same interface as the object it wraps, adding behaviour around delegation.
- Component — The interface shared by the real object and all its decorators.
- Wrapper stack — Several decorators nested around one object, applied outside-in.
- Middleware — Decorators around a request handler, common in web frameworks.