The Decorator Pattern
Shelfwise wants every payment call logged, retried up to three times on network errors, and timed. Production wants all three, the test suite wants none, and the staging server wants only logging. With subclasses that is LoggingRetryingTimingCardGateway and seven cousins. Decorator gets every combination from three small classes.
The rule
Wrap an object in another with the same interface, and add behaviour around the call.
A decorator implements the same interface as the object it wraps, holds a reference to it, and delegates to it, doing extra work before or after.
class LoggingGateway(PaymentGateway):
def __init__(self, inner: PaymentGateway):
self.inner = inner
def charge(self, card, amount):
log(f"charging {amount}")
result = self.inner.charge(card, amount)
log("charge done")
return result
class RetryingGateway(PaymentGateway):
def __init__(self, inner: PaymentGateway, attempts=3):
self.inner = inner
self.attempts = attempts
def charge(self, card, amount):
for attempt in range(self.attempts):
try:
return self.inner.charge(card, amount)
except NetworkError:
if attempt == self.attempts - 1:
raiseBecause each decorator is a PaymentGateway, decorators can wrap each other:
production = LoggingGateway(RetryingGateway(TimingGateway(CardGateway())))
staging = LoggingGateway(CardGateway())
tests = FakeGateway()Checkout receives one PaymentGateway and cannot tell how many layers it has. Three features, freely combined, from three classes: composition over inheritance once more. Each decorator also has a single responsibility, so each is easy to test alone.
Order matters. Here logging is outermost, so one log line surrounds all the retries. Swap LoggingGateway and RetryingGateway, and every attempt gets its own log lines.
Python's @decorator syntax for functions is a close cousin: a function that wraps another function to add behaviour around it, keeping the same way of calling it.
The shortcut
When you want to add a behaviour that goes around an existing call (logging, timing, caching, retries, access checks) and you want to switch it on and off per situation, write a wrapper with the same interface. Then read the stack from outside in: the outer layer runs first on the way in and last on the way out.
The existing interface
BookCatalogwithfind(isbn); the real one,DbCatalog, queries the database.
The trap is a decorator that changes the interface or the meaning of the call, such as returning a different type. Then it is no longer a transparent wrapper, and callers can break.
Drills
Answer fast — trust the rule.
What must a decorator have in common with the object it wraps?
Three optional features (log, retry, time) can be combined freely. How many decorator classes do you need?
In LoggingGateway(RetryingGateway(CardGateway())), which runs first when charge is called?
Which is a legitimate decorator for BookCatalog.find(isbn)?
Remember
- Wrap an object in another with the same interface, and add behaviour around the call.
- Read a stack from outside in: the outer layer runs first on the way in.
- Trap: a "decorator" that changes the interface or meaning is no longer transparent.
