Step 1 Understand it
The problem it solves. You want to add extras like , or to some objects, in different combinations, without writing a for every combination.
Watch it happen
Read the animation as text
- Wrappers, one inside another: Logging(Caching(RealDataSource())). Each implements the same read() and holds the next one in.
- A caller calls read(). The outer wrapper, Logging, writes a log line, then calls the object inside it.
- Caching checks its cache: a miss, so it passes the call inward.
- The real object does the work, and the answer travels back out through each wrapper — Caching keeps a copy on the way.
- Call read() again: Caching answers from its copy and never bothers the real object. Callers can't tell any wrapper is there — they just call read().
Picture it
flowchart LR C["Caller"] -->|"read()"| L["LoggingSource<br/>adds a log line"] -->|"read()"| U["UpperCaseSource<br/>upper-cases"] -->|"read()"| D["DataSource<br/>the real data"]
The idea
Instead of a subclass for every combination (LoggedCachedRetryingClient and so on), you stack small wrappers: a retry wrapper around a cache wrapper around the real . Each wrapper adds one feature and passes the call along.
How it works, step by step
- Start with an object that implements an : read().
- Write a wrapper that implements the same interface and holds another object of that interface.
- In its read(), do the extra work (write a log line, check a cache), then call the inner object's read().
- Wrap again for more features: Logging(Caching(Real())).
- Callers can't tell they're talking to a wrapper; they just call read().
Analogy
Wrapping a present in layers: paper, then ribbon, then a card. It's still the same present underneath, and you can add or skip any layer.
Where the analogy breaks
Gift layers don't affect each other. Code wrappers do, so the order matters: caching outside retries behaves differently from retries outside caching.
Example
Java's file (a BufferedInputStream around a FileInputStream), and Python's @ for logging or timing functions.
Must-know points
- Same interface as the wrapped object, so wrappers can stack.
- Avoids an explosion of subclasses for every feature combination.
- Follows open/closed: add features without editing classes.
- Python's @ syntax is a related idea for functions.
Common mistakes
- Wrappers that change what the method means, rather than just adding to it.
- Wrapping in the wrong order (for example, caching errors).
class DataSource:
def read(self): return "data"
class LoggingSource:
def __init__(self, inner): self.inner = inner
def read(self):
print("reading…")
return self.inner.read()
class UpperCaseSource:
def __init__(self, inner): self.inner = inner
def read(self):
return self.inner.read().upper()
source = LoggingSource(UpperCaseSource(DataSource())) # stack the features
source.read() # prints "reading…", returns "DATA"Step 2 Go further (free, optional)
Optional. The notes above are written to be enough on their own. These free resources give a second explanation and extra practice.
-
Read
Refactoring.Guru, free
-
Read
Real Python
Step 3 Practise
Try each exercise yourself first: on paper, out loud or in code. Then open the answer and compare it with yours.
-
ExplainSay it in plain words in under a minute, with no notes.
Explain Decorator with the present-wrapping picture.
Show answer
You wrap a present in layers: paper, then a ribbon, then a card. Each layer adds one thing; underneath it is still the same present, and you can add or leave out any layer, in any order.
A decorator does that to an object. Each decorator holds the object inside it, has the same (so it looks exactly like the thing it wraps), adds one feature, and passes the call on: a logging layer prints "reading…", an upper-case layer changes the result, a cache layer remembers it. You stack the features you need, LoggingSource(UpperCaseSource(DataSource())), instead of writing a subclass for every combination (LoggedUpperCaseCachedSource…). With three features, that is three small classes instead of seven combinations.
-
DrawSketch the diagram (or class diagram) from memory.
Draw the stack of wrappers around DataSource.
Show answer
The call goes down through each layer to the real source, and the result comes back up through them. Every layer has the same read() method as the thing it wraps, so any layer can wrap any other, and the caller can't tell how many layers there are.
textcaller │ read() ▲ "DATA" ▼ │ LoggingSource prints "reading…" │ │ inner.read() │ "DATA" ▼ │ UpperCaseSource │ upper-cases │ inner.read() │ "data" → "DATA" ▼ │ DataSource ───── returns "data" ──┘ -
ApplyUse it in a real design question and say exactly where it fits.
Write a TimingDecorator and a RetryDecorator for an client class, and stack them in both orders. What changes?
Show answer
Both decorators have the same get() method as the client, so they stack either way. A fake clock and a client that fails twice make the results repeatable.
The order changes what is measured. Timing outside retry measures the whole call, retries included: one entry of 0.3 s, which is what the user waited. Retry outside timing measures each attempt separately: three entries of 0.1 s, which tells you how fast the is but hides that the user waited three times as long. Neither is wrong; they answer different questions, so choose the order on purpose (or keep both).
pythonclass Clock: # a fake clock, so the times are repeatable def __init__(self): self.now = 0.0 class FlakyClient: """Times out twice, then works. Each call takes 0.1 s on the fake clock.""" def __init__(self, clock): self.clock, self.calls = clock, 0 def get(self, url): self.calls += 1 self.clock.now += 0.1 if self.calls <= 2: raise TimeoutError(url) return f"200 OK {url}" class TimingDecorator: def __init__(self, inner, clock, log): self.inner, self.clock, self.log = inner, clock, log def get(self, url): start = self.clock.now try: return self.inner.get(url) finally: self.log.append(f"took {self.clock.now - start:.1f} s") class RetryDecorator: def __init__(self, inner, attempts=3): self.inner, self.attempts = inner, attempts def get(self, url): for attempt in range(1, self.attempts + 1): try: return self.inner.get(url) except TimeoutError: if attempt == self.attempts: raise for order in ("timing outside retry", "retry outside timing"): clock, log = Clock(), [] client = FlakyClient(clock) if order == "timing outside retry": stack = TimingDecorator(RetryDecorator(client), clock, log) else: stack = RetryDecorator(TimingDecorator(client, clock, log)) print(order, "|", stack.get("/orders"), "|", log) # timing outside retry | 200 OK /orders | ['took 0.3 s'] # retry outside timing | 200 OK /orders | ['took 0.1 s', 'took 0.1 s', 'took 0.1 s'] -
Trade-offsSay what it costs, when not to use it, and the alternative.
When do many layers of decorators make debugging hard?
Show answer
• The stack is invisible where it is used: the caller just sees "a client". What it actually does depends on how many layers were wrapped around it, in what order, somewhere else (often in start-up code).
• Order changes behaviour, silently: retry inside a cache behaves differently from cache inside retry, and timing inside or outside retry measures different things.
• Layers change things on the way through: a retry layer hides the first two failures, a cache returns old data, an error-handling layer turns an exception into a default. When the result is wrong, you have to work out which layer produced it.
• Stack get deep, full of near-identical wrapper frames.
• Duplicated layers multiply: a retry around a client that already retries means 3 × 3 = 9 attempts.
• Identity is lost: isinstance checks and attribute access stop working on the wrapped object.Keep the stack short, build it in one obvious place, give each layer a clear name, and have each log what it did.
Step 4 Interview-ready, then prove it
Your interview answer
About 20 to 40 seconds, said out loud. Practise it until it sounds like you, not like notes.
Decorator wraps an object with another that has the same interface and adds one behaviour, like logging, caching or retries. Wrappers can be stacked in any combination, which avoids an explosion of subclasses.
Follow-up questions
Decorator or ?
Inheritance fixes behaviour for a whole class when you write the code. Decorators add it to individual objects at runtime, and combine freely.
Tick each check when you can do it without notes
A topic is finished when you pass all four checks, not when you finish a video. Be strict: if you needed your notes, it is not ticked yet.
- Explain Say it in plain words in under a minute, with no notes.
- Draw Sketch the diagram (or class diagram) from memory.
- Apply Use it in a real design question and say exactly where it fits.
- Trade-offs Say what it costs, when not to use it, and the alternative.
0 of 4 checks ticked.
Saved in this browser. Back it up or move it to another device.