Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
Design Patterns  ›  Capstone Project

The Decorator Pattern

GoF Design Patterns 16 minDecorator wraps an object in another object with the same interface to add behaviour around it, so features can be stacked without subclasses.

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:
                    raise

Because 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.

Adding a cache to book lookupsStep 1 of 4
  1. The existing interface

    BookCatalog with find(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.

quick drill

What must a decorator have in common with the object it wraps?

quick drill

Three optional features (log, retry, time) can be combined freely. How many decorator classes do you need?

quick drill

In LoggingGateway(RetryingGateway(CardGateway())), which runs first when charge is called?

quick drill

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.

Practice — level up

0/1 solved

Write your answer, then compare it with a rubric and a model answer.

ExplainWrap the payment gateway

Shelfwise wants payment calls to be logged, retried up to three times on network errors, and timed — but not every environment wants all three. Explain how the Decorator pattern delivers this, show how the objects are stacked, and compare it with making subclasses.