Add behaviour to an object by wrapping it in another object that has the same interface. Wrappers stack, and the order you stack them in changes the answer.
Open a delivery app at night and look at the fee. It starts from the distance. On top of that, a small-cart fee may apply, a late-night fee may apply, and when the restaurant is busy the whole thing may be doubled. Each add-on is a separate rule that somebody added later, and any mix can apply to one order. The same shape shows up in code that calls a web service: a retry and a log line around the same client, chosen per call site.
The Decorator pattern is the answer to that shape. A decorator is an object that has the same interface as the thing it wraps and holds one of them. It forwards the call, and adds something before or after. Because it looks like the thing it wraps, you can wrap it again. This lesson builds the fee stack, shows why the order of the wrappers matters, and then does the two things most Decorator lessons skip: what a wrapper does to object identity and equality, and how the pattern relates to Python's @decorator syntax, which is related but not the same thing. Three case studies use it: the parking lot lists "the first 30 minutes free" as a follow-up, ride sharing wraps any fare in SurgeFare, and the pub-sub logger wraps any sink in an AsyncSink. The Strategy lesson left a follow-up open, a 20-rupee late-night surcharge on top of whichever fee rule applies, and this is where it gets built.
A pizza gets a box, then an insulated bag. Each layer adds something, the courier still hands over one thing, and the order of the layers matters.
The first version: a flag for every add-on#
The first fee ships as a function. Then each team adds its rule, and each rule is a flag:
# first_version.py: one function with a flag per add-on; the order of the rules is baked in
def delivery_fee(km: int, cart: int, hour: int, surge: bool = False, late_night: bool = False, small_cart: bool = False) -> int:
fee = 20 + 10 * km # the distance fee: 20 plus 10 a km
if small_cart and cart < 199:
fee += 15
if late_night and (hour >= 23 or hour < 5):
fee += 20
if surge:
fee *= 2 # surge is always last here, so it doubles the add-ons too
return fee
print("plain:", delivery_fee(5, 180, 23)) # 5 km, a 180 rupee cart, 11 pm
print("small cart and late night:", delivery_fee(5, 180, 23, small_cart=True, late_night=True))
print("all three:", delivery_fee(5, 180, 23, surge=True, late_night=True, small_cart=True))
print("2 km, a 450 rupee cart, 1 pm, all three:", delivery_fee(2, 450, 13, surge=True, late_night=True, small_cart=True))// FirstVersion.java: one method with a flag per add-on; the order of the rules is baked in
class FirstVersion {
static int deliveryFee(int km, int cart, int hour, boolean surge, boolean lateNight, boolean smallCart) {
int fee = 20 + 10 * km; // the distance fee: 20 plus 10 a km
if (smallCart && cart < 199) fee += 15;
if (lateNight && (hour >= 23 || hour < 5)) fee += 20;
if (surge) fee *= 2; // surge is always last here, so it doubles the add-ons too
return fee;
}
public static void main(String[] args) {
System.out.println("plain: " + deliveryFee(5, 180, 23, false, false, false)); // 5 km, a 180 rupee cart, 11 pm
System.out.println("small cart and late night: " + deliveryFee(5, 180, 23, false, true, true));
System.out.println("all three: " + deliveryFee(5, 180, 23, true, true, true));
System.out.println("2 km, a 450 rupee cart, 1 pm, all three: " + deliveryFee(2, 450, 13, true, true, true));
}
}// first_version.cpp: one function with a flag per add-on; the order of the rules is baked in
#include <iostream>
int delivery_fee(int km, int cart, int hour, bool surge, bool late_night, bool small_cart) {
int fee = 20 + 10 * km; // the distance fee: 20 plus 10 a km
if (small_cart && cart < 199) fee += 15;
if (late_night && (hour >= 23 || hour < 5)) fee += 20;
if (surge) fee *= 2; // surge is always last here, so it doubles the add-ons too
return fee;
}
int main() {
std::cout << "plain: " << delivery_fee(5, 180, 23, false, false, false) << "\n"; // 5 km, a 180 rupee cart, 11 pm
std::cout << "small cart and late night: " << delivery_fee(5, 180, 23, false, true, true) << "\n";
std::cout << "all three: " << delivery_fee(5, 180, 23, true, true, true) << "\n";
std::cout << "2 km, a 450 rupee cart, 1 pm, all three: " << delivery_fee(2, 450, 13, true, true, true) << "\n";
}// firstVersion.js: one function with a flag per add-on; the order of the rules is baked in
function deliveryFee(km, cart, hour, { surge = false, lateNight = false, smallCart = false } = {}) {
let fee = 20 + 10 * km; // the distance fee: 20 plus 10 a km
if (smallCart && cart < 199) fee += 15;
if (lateNight && (hour >= 23 || hour < 5)) fee += 20;
if (surge) fee *= 2; // surge is always last here, so it doubles the add-ons too
return fee;
}
console.log("plain:", deliveryFee(5, 180, 23)); // 5 km, a 180 rupee cart, 11 pm
console.log("small cart and late night:", deliveryFee(5, 180, 23, { smallCart: true, lateNight: true }));
console.log("all three:", deliveryFee(5, 180, 23, { surge: true, lateNight: true, smallCart: true }));
console.log("2 km, a 450 rupee cart, 1 pm, all three:", deliveryFee(2, 450, 13, { surge: true, lateNight: true, smallCart: true }));plain: 70
small cart and late night: 105
all three: 210
2 km, a 450 rupee cart, 1 pm, all three: 80The rules, so the numbers make sense: the distance fee is 20 rupees plus 10 for every km, so 5 km is 70. A cart under 199 rupees adds 15. From 11 pm to 5 am a late-night fee adds 20. A surge doubles the fee. For the 5 km, 180-rupee, 11 pm order: 70, plus 15 is 85, plus 20 is 105, doubled is 210, which is the last line but one. The final line is the same three flags on a 2 km, 450-rupee order at 1 pm: the small-cart and late-night rules don't apply, only the surge does, and 40 doubled is 80.
It works, and one function is easy to read. The trouble is the next add-on:
- Every add-on edits the same function, and every caller changes with it: a rain fee is a fourth flag, and in a language without keyword arguments the call sites read
deliveryFee(5, 180, 23, true, false, true). - The order is baked in. Surge is always last, so it doubles the add-ons. If finance decides surge should apply to the distance fee only, you need a second function or a new flag.
- The tests multiply. Four flags are 2⁴ = 16 combinations to think about, and each one is in the same function as the others.
- The subclass escape does not help. One class per combination is 2³ = 8 classes for 3 add-ons (the plain fee included), and 16 once the order of the rules matters: 1 with none, 3 with one, 6 with two and 6 with three. It is the explosion from the first lesson again.
Before the refactor: what is the part that varies?
A rain add-on is coming. In the flag version, what do you edit, and how many combinations of flags must the tests consider now?
One strong answer
You edit the function and every call site, and the tests face 2⁴ = 16 combinations. The part that varies is which add-ons apply, and in which order, and the code has fixed both inside one function. The pattern's move is to make each add-on a small object that takes any fee rule and returns a fee rule, so that "which ones, in which order" becomes a choice the caller makes, not code someone edits.
The pattern#
The book that named it (Design Patterns, 1994, by Gamma, Helm, Johnson and Vlissides) states the intent of Decorator as: "Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality." The book's other name for it is Wrapper, the same second name it gives Adapter, so "wrapper" alone doesn't say which one you mean.
| Role | In the fee code | Its one job |
|---|---|---|
| Component (the interface) | FeeRule with fee(order) |
What every rule and every wrapper can do |
| Concrete component | DistanceFee |
The base object that does the real work |
| Decorator | SmallCart, LateNight, Surge |
Implements FeeRule and holds a FeeRule: forwards the call and adds its own behaviour |
| Client | The code that asks for a fee (here the program's loop) | Holds a FeeRule and never knows how many layers are inside |
The book also draws an abstract Decorator class that does the forwarding, with concrete decorators extending it. In a program this small each decorator holds inner itself, which is the same idea with one class less.
%% title: Decorator: each wrapper is a FeeRule that has a FeeRule
classDiagram
class FeeRule {
<<interface>>
+fee(order) int
}
class DistanceFee
class SmallCart
class LateNight
class Surge
class Client
FeeRule <|.. DistanceFee
FeeRule <|.. SmallCart
FeeRule <|.. LateNight
FeeRule <|.. Surge
SmallCart --> FeeRule : wraps
LateNight --> FeeRule : wraps
Surge --> FeeRule : wraps
Client --> FeeRule : asks for the feeRead the arrows. Every decorator has two arrows: one up, because it is a FeeRule, and one back at the same interface, because it has a FeeRule. That loop is the whole pattern. It is what lets you wrap a wrapper, and it is why the client can't tell a bare DistanceFee from a stack of four.
A call to the outermost wrapper travels inward to the base. The answers come back out, each wrapper adding its part, and the client never knows.
The code: stack the wrappers#
Here is the refactor in all four languages. Each add-on becomes a class, a small build function wraps them in the order a list names them, and the program prints each stack's own description next to its fee:
# decorators.py: three add-ons wrap any fee rule; stack them in any order
from abc import ABC, abstractmethod
class Order:
def __init__(self, km: int, cart: int, hour: int) -> None:
self.km, self.cart, self.hour = km, cart, hour
class FeeRule(ABC): # the component interface: every rule and every wrapper has it
@abstractmethod
def fee(self, order: Order) -> int: ...
@abstractmethod
def label(self) -> str: ...
class DistanceFee(FeeRule): # the concrete component: 20 plus 10 a km
def fee(self, order: Order) -> int:
return 20 + 10 * order.km
def label(self) -> str:
return "Distance"
class SmallCart(FeeRule): # a decorator is a FeeRule that has a FeeRule
def __init__(self, inner: FeeRule) -> None:
self.inner = inner
def fee(self, order: Order) -> int:
return self.inner.fee(order) + (15 if order.cart < 199 else 0)
def label(self) -> str:
return f"SmallCart({self.inner.label()})"
class LateNight(FeeRule):
def __init__(self, inner: FeeRule) -> None:
self.inner = inner
def fee(self, order: Order) -> int:
return self.inner.fee(order) + (20 if order.hour >= 23 or order.hour < 5 else 0)
def label(self) -> str:
return f"LateNight({self.inner.label()})"
class Surge(FeeRule):
def __init__(self, inner: FeeRule, times: int) -> None:
self.inner, self.times = inner, times
def fee(self, order: Order) -> int:
return self.inner.fee(order) * self.times
def label(self) -> str:
return f"Surge({self.inner.label()})"
MAKERS = {"small-cart": SmallCart, "late-night": LateNight, "surge": lambda inner: Surge(inner, 2)}
def build(names) -> FeeRule: # the first name is wrapped first, so it sits closest to the distance fee
rule: FeeRule = DistanceFee()
for name in names:
rule = MAKERS[name](rule)
return rule
order_a = Order(5, 180, 23) # 5 km, a 180 rupee cart, 11 pm
order_b = Order(2, 450, 13) # 2 km, a 450 rupee cart, 1 pm
plain = build([])
print(f"A, {plain.label()} = {plain.fee(order_a)}")
for names in (["small-cart", "late-night", "surge"], ["surge", "small-cart", "late-night"]):
rule = build(names)
print(f"A, {rule.label()} = {rule.fee(order_a)}")
rule = build(["small-cart", "late-night", "surge"])
print(f"B, {rule.label()} = {rule.fee(order_b)}")// Decorators.java: three add-ons wrap any fee rule; stack them in any order
import java.util.List;
import java.util.Map;
import java.util.function.UnaryOperator;
class Decorators {
record Order(int km, int cart, int hour) {}
interface FeeRule { // the component interface: every rule and every wrapper has it
int fee(Order order);
String label();
}
static final class DistanceFee implements FeeRule { // the concrete component: 20 plus 10 a km
public int fee(Order order) { return 20 + 10 * order.km(); }
public String label() { return "Distance"; }
}
static final class SmallCart implements FeeRule { // a decorator is a FeeRule that has a FeeRule
private final FeeRule inner;
SmallCart(FeeRule inner) { this.inner = inner; }
public int fee(Order order) { return inner.fee(order) + (order.cart() < 199 ? 15 : 0); }
public String label() { return "SmallCart(" + inner.label() + ")"; }
}
static final class LateNight implements FeeRule {
private final FeeRule inner;
LateNight(FeeRule inner) { this.inner = inner; }
public int fee(Order order) { return inner.fee(order) + (order.hour() >= 23 || order.hour() < 5 ? 20 : 0); }
public String label() { return "LateNight(" + inner.label() + ")"; }
}
static final class Surge implements FeeRule {
private final FeeRule inner;
private final int times;
Surge(FeeRule inner, int times) {
this.inner = inner;
this.times = times;
}
public int fee(Order order) { return inner.fee(order) * times; }
public String label() { return "Surge(" + inner.label() + ")"; }
}
static final Map<String, UnaryOperator<FeeRule>> MAKERS = Map.of(
"small-cart", SmallCart::new, "late-night", LateNight::new, "surge", inner -> new Surge(inner, 2));
static FeeRule build(List<String> names) { // the first name is wrapped first, so it sits closest to the distance fee
FeeRule rule = new DistanceFee();
for (String name : names) rule = MAKERS.get(name).apply(rule);
return rule;
}
public static void main(String[] args) {
Order orderA = new Order(5, 180, 23); // 5 km, a 180 rupee cart, 11 pm
Order orderB = new Order(2, 450, 13); // 2 km, a 450 rupee cart, 1 pm
FeeRule plain = build(List.of());
System.out.println("A, " + plain.label() + " = " + plain.fee(orderA));
for (List<String> names : List.of(List.of("small-cart", "late-night", "surge"), List.of("surge", "small-cart", "late-night"))) {
FeeRule rule = build(names);
System.out.println("A, " + rule.label() + " = " + rule.fee(orderA));
}
FeeRule rule = build(List.of("small-cart", "late-night", "surge"));
System.out.println("B, " + rule.label() + " = " + rule.fee(orderB));
}
}// decorators.cpp: three add-ons wrap any fee rule; stack them in any order
#include <functional>
#include <iostream>
#include <map>
#include <memory>
#include <string>
#include <utility>
#include <vector>
struct Order {
int km;
int cart;
int hour;
};
class FeeRule { // the component interface: every rule and every wrapper has it
public:
virtual ~FeeRule() = default;
virtual int fee(const Order& order) const = 0;
virtual std::string label() const = 0;
};
using Rule = std::unique_ptr<FeeRule>;
class DistanceFee : public FeeRule { // the concrete component: 20 plus 10 a km
public:
int fee(const Order& order) const override { return 20 + 10 * order.km; }
std::string label() const override { return "Distance"; }
};
class SmallCart : public FeeRule { // a decorator is a FeeRule that has a FeeRule
Rule inner_;
public:
explicit SmallCart(Rule inner) : inner_(std::move(inner)) {}
int fee(const Order& order) const override { return inner_->fee(order) + (order.cart < 199 ? 15 : 0); }
std::string label() const override { return "SmallCart(" + inner_->label() + ")"; }
};
class LateNight : public FeeRule {
Rule inner_;
public:
explicit LateNight(Rule inner) : inner_(std::move(inner)) {}
int fee(const Order& order) const override { return inner_->fee(order) + (order.hour >= 23 || order.hour < 5 ? 20 : 0); }
std::string label() const override { return "LateNight(" + inner_->label() + ")"; }
};
class Surge : public FeeRule {
Rule inner_;
int times_;
public:
Surge(Rule inner, int times) : inner_(std::move(inner)), times_(times) {}
int fee(const Order& order) const override { return inner_->fee(order) * times_; }
std::string label() const override { return "Surge(" + inner_->label() + ")"; }
};
const std::map<std::string, std::function<Rule(Rule)>> MAKERS = {
{"small-cart", [](Rule inner) -> Rule { return std::make_unique<SmallCart>(std::move(inner)); }},
{"late-night", [](Rule inner) -> Rule { return std::make_unique<LateNight>(std::move(inner)); }},
{"surge", [](Rule inner) -> Rule { return std::make_unique<Surge>(std::move(inner), 2); }},
};
Rule build(const std::vector<std::string>& names) { // the first name is wrapped first, so it sits closest to the distance fee
Rule rule = std::make_unique<DistanceFee>();
for (const auto& name : names) rule = MAKERS.at(name)(std::move(rule));
return rule;
}
int main() {
Order order_a{5, 180, 23}; // 5 km, a 180 rupee cart, 11 pm
Order order_b{2, 450, 13}; // 2 km, a 450 rupee cart, 1 pm
Rule plain = build({});
std::cout << "A, " << plain->label() << " = " << plain->fee(order_a) << "\n";
for (const auto& names : std::vector<std::vector<std::string>>{{"small-cart", "late-night", "surge"}, {"surge", "small-cart", "late-night"}}) {
Rule rule = build(names);
std::cout << "A, " << rule->label() << " = " << rule->fee(order_a) << "\n";
}
Rule rule = build({"small-cart", "late-night", "surge"});
std::cout << "B, " << rule->label() << " = " << rule->fee(order_b) << "\n";
}// decorators.js: three add-ons wrap any fee rule; stack them in any order
// the component interface has no keyword in JavaScript: every rule and every wrapper has fee(order) and label()
class DistanceFee { // the concrete component: 20 plus 10 a km
fee(order) { return 20 + 10 * order.km; }
label() { return "Distance"; }
}
class SmallCart { // a decorator is a FeeRule that has a FeeRule
constructor(inner) { this.inner = inner; }
fee(order) { return this.inner.fee(order) + (order.cart < 199 ? 15 : 0); }
label() { return `SmallCart(${this.inner.label()})`; }
}
class LateNight {
constructor(inner) { this.inner = inner; }
fee(order) { return this.inner.fee(order) + (order.hour >= 23 || order.hour < 5 ? 20 : 0); }
label() { return `LateNight(${this.inner.label()})`; }
}
class Surge {
constructor(inner, times) {
this.inner = inner;
this.times = times;
}
fee(order) { return this.inner.fee(order) * this.times; }
label() { return `Surge(${this.inner.label()})`; }
}
const MAKERS = {
"small-cart": (inner) => new SmallCart(inner),
"late-night": (inner) => new LateNight(inner),
surge: (inner) => new Surge(inner, 2),
};
function build(names) { // the first name is wrapped first, so it sits closest to the distance fee
let rule = new DistanceFee();
for (const name of names) rule = MAKERS[name](rule);
return rule;
}
const orderA = { km: 5, cart: 180, hour: 23 }; // 5 km, a 180 rupee cart, 11 pm
const orderB = { km: 2, cart: 450, hour: 13 }; // 2 km, a 450 rupee cart, 1 pm
const plain = build([]);
console.log(`A, ${plain.label()} = ${plain.fee(orderA)}`);
for (const names of [["small-cart", "late-night", "surge"], ["surge", "small-cart", "late-night"]]) {
const rule = build(names);
console.log(`A, ${rule.label()} = ${rule.fee(orderA)}`);
}
const rule = build(["small-cart", "late-night", "surge"]);
console.log(`B, ${rule.label()} = ${rule.fee(orderB)}`);A, Distance = 70
A, Surge(LateNight(SmallCart(Distance))) = 210
A, LateNight(SmallCart(Surge(Distance))) = 175
B, Surge(LateNight(SmallCart(Distance))) = 80Check one row by hand. The first stack wraps SmallCart around DistanceFee, LateNight around that and Surge around that, so a call goes in from the outside: Surge asks LateNight, which asks SmallCart, which asks DistanceFee, which answers 70. Coming back out, SmallCart adds 15 (the cart is 180, under 199) to make 85, LateNight adds 20 (it is 11 pm) to make 105, and Surge doubles it to 210, the same number as the flag version. Order B has a 450-rupee cart at 1 pm, so neither add-on applies and only the doubling does: 40 doubled is 80.
The second stack puts the same three in a different order, and the answer is 175. That is the next section. First, what each language brings:
- Python uses an
ABCfor the interface, but a decorator only needs the two methods, so duck typing is enough. Closures play the same part for functions, which is the@decoratorsection below. - Java uses an
interfaceandfinalclasses that hold the inner rule in afinalfield.java.iois the classic case in the standard library:new BufferedInputStream(new FileInputStream(path))wraps one stream in another, and both are anInputStream. - C++ uses an abstract base with a virtual destructor, and each wrapper owns its inner rule in a
unique_ptr, so the stack is built by moving one into the next and the outermost object frees everything. Every layer is one more virtual call. - JavaScript has no interface keyword, so the component is a convention (
feeandlabel), and a wrapper is any object that has them and holds an inner one.
A stack is built from a list of names. Each name wraps the rule so far: the first sits closest to the base, the last is outermost.
Wrapper order matters#
The two stacks hold the same pieces. Add the numbers by hand to see why they differ:
| Order of wrapping (innermost first) | Steps | Fee |
|---|---|---|
| Small cart, late night, then surge | 70 + 15 = 85, + 20 = 105, × 2 | 210 |
| Surge, small cart, then late night | 70 × 2 = 140, + 15 = 155, + 20 | 175 |
The surge doubled the add-ons in the first stack and only the distance fee in the second, and the difference is 35 rupees: the two add-ons that apply (15 and 20) are doubled in one order and not in the other. Neither order is wrong. It is a business rule: should a surge multiply everything the customer owes, or only the base fee? The ride-sharing case study makes the first choice for its fares: SurgeFare wraps the whole tariff. What matters is that the order is visible: it is the list ["small-cart", "late-night", "surge"], which can sit in a config file and be tested, where the flag version hid it in three if statements.
The retry-and-logging program makes the same point with behaviour instead of arithmetic. The same flaky client fails its first two calls and answers on the third. Retry makes up to 3 attempts. The only difference between the two stacks is who is on the outside:
# http_wrappers.py: retry and logging wrap any client; the order decides what each one sees
from abc import ABC, abstractmethod
class Timeout(Exception):
pass
class Client(ABC): # the component interface
@abstractmethod
def get(self, path: str) -> str: ...
class FlakyClient(Client): # stands in for the real HTTP client: its first fail_first calls time out
def __init__(self, fail_first: int) -> None:
self.fail_first, self.calls = fail_first, 0
def get(self, path: str) -> str:
self.calls += 1
if self.calls <= self.fail_first:
raise Timeout("no answer")
return "menu page"
class Retry(Client): # tries the inner client up to `attempts` times
def __init__(self, inner: Client, attempts: int) -> None:
self.inner, self.attempts = inner, attempts
def get(self, path: str) -> str:
for attempt in range(self.attempts):
try:
return self.inner.get(path)
except Timeout:
if attempt == self.attempts - 1:
raise
raise AssertionError("attempts must be at least 1")
class Logging(Client): # writes one line for every call it sees
def __init__(self, inner: Client, log) -> None:
self.inner, self.log = inner, log
def get(self, path: str) -> str:
try:
result = self.inner.get(path)
except Timeout:
self.log.append(f"GET {path} -> timeout")
raise
self.log.append(f"GET {path} -> ok")
return result
def run(label: str, stack) -> None: # stack(client, log) builds the wrappers around a client that fails twice
log = []
stack(FlakyClient(2), log).get("/menu")
print(f"{label}: {len(log)} line{'' if len(log) == 1 else 's'}")
for line in log:
print(" " + line)
run("logging outside retry", lambda client, log: Logging(Retry(client, 3), log))
run("logging inside retry", lambda client, log: Retry(Logging(client, log), 3))
inner = FlakyClient(0)
wrapper, other = Retry(inner, 3), Retry(inner, 3)
print("a wrapper is the same object as the client it wraps:", str(wrapper is inner).lower())
print("a wrapper is a Client:", str(isinstance(wrapper, Client)).lower())
print("a wrapper is a FlakyClient:", str(isinstance(wrapper, FlakyClient)).lower())
print("two wrappers over one client are the same object:", str(wrapper is other).lower())// HttpWrappers.java: retry and logging wrap any client; the order decides what each one sees
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiFunction;
class HttpWrappers {
static final class Timeout extends RuntimeException {
Timeout(String why) { super(why); }
}
interface Client { // the component interface
String get(String path);
}
static final class FlakyClient implements Client { // stands in for the real HTTP client: its first failFirst calls time out
private final int failFirst;
private int calls = 0;
FlakyClient(int failFirst) { this.failFirst = failFirst; }
public String get(String path) {
calls++;
if (calls <= failFirst) throw new Timeout("no answer");
return "menu page";
}
}
static final class Retry implements Client { // tries the inner client up to attempts times
private final Client inner;
private final int attempts;
Retry(Client inner, int attempts) {
this.inner = inner;
this.attempts = attempts;
}
public String get(String path) {
for (int attempt = 0; attempt < attempts; attempt++) {
try {
return inner.get(path);
} catch (Timeout e) {
if (attempt == attempts - 1) throw e;
}
}
throw new IllegalStateException("attempts must be at least 1");
}
}
static final class Logging implements Client { // writes one line for every call it sees
private final Client inner;
private final List<String> log;
Logging(Client inner, List<String> log) {
this.inner = inner;
this.log = log;
}
public String get(String path) {
String result;
try {
result = inner.get(path);
} catch (Timeout e) {
log.add("GET " + path + " -> timeout");
throw e;
}
log.add("GET " + path + " -> ok");
return result;
}
}
static void run(String label, BiFunction<Client, List<String>, Client> stack) { // stack builds the wrappers around a client that fails twice
List<String> log = new ArrayList<>();
stack.apply(new FlakyClient(2), log).get("/menu");
System.out.println(label + ": " + log.size() + " line" + (log.size() == 1 ? "" : "s"));
for (String line : log) System.out.println(" " + line);
}
public static void main(String[] args) {
run("logging outside retry", (client, log) -> new Logging(new Retry(client, 3), log));
run("logging inside retry", (client, log) -> new Retry(new Logging(client, log), 3));
FlakyClient inner = new FlakyClient(0);
Client wrapper = new Retry(inner, 3);
Client other = new Retry(inner, 3);
System.out.println("a wrapper is the same object as the client it wraps: " + (wrapper == inner));
System.out.println("a wrapper is a Client: " + (wrapper instanceof Client));
System.out.println("a wrapper is a FlakyClient: " + (wrapper instanceof FlakyClient));
System.out.println("two wrappers over one client are the same object: " + (wrapper == other));
}
}// http_wrappers.cpp: retry and logging wrap any client; the order decides what each one sees
#include <functional>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
struct Timeout : std::runtime_error {
explicit Timeout(const std::string& why) : std::runtime_error(why) {}
};
class Client { // the component interface
public:
virtual ~Client() = default;
virtual std::string get(const std::string& path) = 0;
};
using Ptr = std::shared_ptr<Client>; // shared, so two wrappers can sit over one client and the caller keeps a handle
class FlakyClient : public Client { // stands in for the real HTTP client: its first fail_first calls time out
int fail_first_;
int calls_ = 0;
public:
explicit FlakyClient(int fail_first) : fail_first_(fail_first) {}
std::string get(const std::string&) override {
++calls_;
if (calls_ <= fail_first_) throw Timeout("no answer");
return "menu page";
}
};
class Retry : public Client { // tries the inner client up to attempts times
Ptr inner_;
int attempts_;
public:
Retry(Ptr inner, int attempts) : inner_(std::move(inner)), attempts_(attempts) {}
std::string get(const std::string& path) override {
for (int attempt = 0; attempt < attempts_; ++attempt) {
try {
return inner_->get(path);
} catch (const Timeout&) {
if (attempt == attempts_ - 1) throw;
}
}
throw std::logic_error("attempts must be at least 1");
}
};
class Logging : public Client { // writes one line for every call it sees
Ptr inner_;
std::vector<std::string>& log_;
public:
Logging(Ptr inner, std::vector<std::string>& log) : inner_(std::move(inner)), log_(log) {}
std::string get(const std::string& path) override {
std::string result;
try {
result = inner_->get(path);
} catch (const Timeout&) {
log_.push_back("GET " + path + " -> timeout");
throw;
}
log_.push_back("GET " + path + " -> ok");
return result;
}
};
using Stack = std::function<Ptr(Ptr, std::vector<std::string>&)>;
void run(const std::string& label, const Stack& stack) { // stack builds the wrappers around a client that fails twice
std::vector<std::string> log;
stack(std::make_shared<FlakyClient>(2), log)->get("/menu");
std::cout << label << ": " << log.size() << " line" << (log.size() == 1 ? "" : "s") << "\n";
for (const auto& line : log) std::cout << " " << line << "\n";
}
int main() {
run("logging outside retry", [](Ptr client, std::vector<std::string>& log) -> Ptr {
return std::make_shared<Logging>(std::make_shared<Retry>(client, 3), log);
});
run("logging inside retry", [](Ptr client, std::vector<std::string>& log) -> Ptr {
return std::make_shared<Retry>(std::make_shared<Logging>(client, log), 3);
});
Ptr inner = std::make_shared<FlakyClient>(0);
Retry wrapper(inner, 3);
Retry other(inner, 3);
std::cout << std::boolalpha;
std::cout << "a wrapper is the same object as the client it wraps: " << (static_cast<Client*>(&wrapper) == inner.get()) << "\n";
std::cout << "a wrapper is a Client: " << (dynamic_cast<Client*>(&wrapper) != nullptr) << "\n";
std::cout << "a wrapper is a FlakyClient: " << (dynamic_cast<FlakyClient*>(static_cast<Client*>(&wrapper)) != nullptr) << "\n";
std::cout << "two wrappers over one client are the same object: " << (static_cast<Client*>(&wrapper) == static_cast<Client*>(&other)) << "\n";
}// httpWrappers.js: retry and logging wrap any client; the order decides what each one sees
class Timeout extends Error {}
class Client { // the component interface: a base class gives the wrappers something to be an instance of
get(path) { throw new Error("not implemented"); }
}
class FlakyClient extends Client { // stands in for the real HTTP client: its first failFirst calls time out
constructor(failFirst) {
super();
this.failFirst = failFirst;
this.calls = 0;
}
get(path) {
this.calls += 1;
if (this.calls <= this.failFirst) throw new Timeout("no answer");
return "menu page";
}
}
class Retry extends Client { // tries the inner client up to attempts times
constructor(inner, attempts) {
super();
this.inner = inner;
this.attempts = attempts;
}
get(path) {
for (let attempt = 0; attempt < this.attempts; attempt++) {
try {
return this.inner.get(path);
} catch (e) {
if (!(e instanceof Timeout) || attempt === this.attempts - 1) throw e;
}
}
throw new Error("attempts must be at least 1");
}
}
class Logging extends Client { // writes one line for every call it sees
constructor(inner, log) {
super();
this.inner = inner;
this.log = log;
}
get(path) {
let result;
try {
result = this.inner.get(path);
} catch (e) {
if (e instanceof Timeout) this.log.push(`GET ${path} -> timeout`);
throw e;
}
this.log.push(`GET ${path} -> ok`);
return result;
}
}
function run(label, stack) { // stack(client, log) builds the wrappers around a client that fails twice
const log = [];
stack(new FlakyClient(2), log).get("/menu");
console.log(`${label}: ${log.length} line${log.length === 1 ? "" : "s"}`);
for (const line of log) console.log(" " + line);
}
run("logging outside retry", (client, log) => new Logging(new Retry(client, 3), log));
run("logging inside retry", (client, log) => new Retry(new Logging(client, log), 3));
const inner = new FlakyClient(0);
const wrapper = new Retry(inner, 3);
const other = new Retry(inner, 3);
console.log("a wrapper is the same object as the client it wraps:", wrapper === inner);
console.log("a wrapper is a Client:", wrapper instanceof Client);
console.log("a wrapper is a FlakyClient:", wrapper instanceof FlakyClient);
console.log("two wrappers over one client are the same object:", wrapper === other);logging outside retry: 1 line
GET /menu -> ok
logging inside retry: 3 lines
GET /menu -> timeout
GET /menu -> timeout
GET /menu -> ok
a wrapper is the same object as the client it wraps: false
a wrapper is a Client: true
a wrapper is a FlakyClient: false
two wrappers over one client are the same object: falseRead the first half. With logging outside the retry, logging sees one call, because the retry hides the two failures from it: 1 line. With logging inside the retry, logging sees every attempt: 3 lines, two timeouts and one success. Each answers a different question: "how many requests did the app make?" or "how many times did we hit the network?". The rule for any stack: the outermost decorator sees the call first and the result last, and the innermost sees what the base sees. The same rule decides whether a timeout sits inside a retry (every attempt gets its own time limit) or outside it (the whole sequence shares one).
Which way round?
You want exactly one log line per customer request, however many retries it took. Do you build Logging(Retry(client)) or Retry(Logging(client))?
One strong answer
Logging(Retry(client)). The outer wrapper sees one call, however many attempts the inner one makes: the first half of the output shows 1 line. Put the logging inside the retry and you get one line per attempt, which is right for a different question ("how many times did we hit the network?").
The same base fee and the same three add-ons give 210 or 175 depending on the wrapping order: surge doubles the add-ons only when it is outside them.
Identity and equality: a wrapper is not what it wraps#
The second half of the output is the part most lessons leave out. Four lines, the same in every language:
- A wrapper is not the same object as the client it wraps:
false. - It is a
Client:true, because it implements the interface. - It is not a
FlakyClient:false, because it is a different class that merely holds one. - Two wrappers over one client are not the same object:
false.
The book says it plainly: a decorated component is not identical to the component, so you shouldn't rely on object identity when you use decorators. Each line is a way real code breaks:
- Type checks on the concrete class fail.
isinstance(x, FlakyClient)in Python,instanceofin Java and JavaScript anddynamic_castin C++ all answer no for a wrapper, so code that asks "is this the real client?" before touching it silently takes the other branch. Ask for an interface, not a class. - Equality and hashing are by identity by default (C++ has no default
==for a class at all, so you end up comparing addresses). Put a wrapper in a set or use it as a dictionary key and two wrappers over the same client are two entries. If you define__eq__(orequalsandhashCode, oroperator==) on a wrapper, don't make it equal to what it wraps:wrapper == innerandinner == wrapperwould have to agree, and you can't editinner's side. JavaScript's===can't be changed at all. - Only the interface is forwarded.
FlakyClient.callsisn't part ofClient, so a holder of the wrapper can't read it. The pub-sub logger shows this in the open:AsyncSinkhas aclose()and adroppedcounter thatSinkdoesn't define, so the code keeps a reference to theAsyncSinkitself and callsclose()on that. Keep a handle on the layer you need, or add the method to the interface. - Every method must be forwarded. A wide interface makes each decorator a page of boilerplate, and a method added later skips every decorator that forgot it.
A wrapper implements the interface but is not the concrete class it holds, so a check for the concrete class fails. Ask for the interface instead.
Decorator and Python's @decorator#
Python has a syntax called a decorator, and people do conflate it with the pattern. The two are related, but not the same thing. A short program separates what @ does from what the pattern does:
# at_decorator.py: Python's @ is syntax for fee = wrapper(fee); a wrapper is only one thing it can do
import functools
calls = []
def logged(fn): # a wrapper: same arguments in, same result out, one extra behaviour
@functools.wraps(fn) # copies __name__ and __doc__, and sets __wrapped__
def wrapper(*args, **kwargs):
calls.append("log:" + fn.__name__)
return fn(*args, **kwargs)
return wrapper
def doubled(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
calls.append("double:" + fn.__name__)
return fn(*args, **kwargs) * 2
return wrapper
def forgetful(fn): # a wrapper that forgets functools.wraps
def wrapper(*args, **kwargs):
return fn(*args, **kwargs)
return wrapper
ROUTES = {}
def route(path): # a registering decorator: it returns the same function, so nothing is wrapped
def register(fn):
ROUTES[path] = fn
return fn
return register
@logged
@doubled
def fee(km):
return 20 + 10 * km
def fee_by_hand(km):
return 20 + 10 * km
fee_by_hand = logged(doubled(fee_by_hand)) # what the two @ lines above mean
@forgetful
def plain(km):
return km
@route("/fee")
def fee_page():
return "fee page"
print("fee(5) with @logged over @doubled:", fee(5))
print("call order:", ", ".join(calls))
print("same as wrapping by hand:", fee_by_hand(5) == fee(5))
print("name kept by functools.wraps:", fee.__name__, "| without it:", plain.__name__)
print("the wrapped function is still reachable:", fee.__wrapped__.__wrapped__(5))
print("@route returns the same function:", ROUTES["/fee"] is fee_page)fee(5) with @logged over @doubled: 140
call order: log:fee, double:fee
same as wrapping by hand: True
name kept by functools.wraps: fee | without it: wrapper
the wrapped function is still reachable: 70
@route returns the same function: TrueRead the output line by line:
@is syntax forfee = wrapper(fee). The@loggedover@doubledlines meanlogged(doubled(fee)), and the "same as wrapping by hand" line shows the two spellings agree. Stack order: the decorator nearest thedefis applied first, so it is the innermost. The call order line shows the outermost,logged, running first.- When it wraps, it is the Decorator pattern applied to a function.
loggedtakes a callable and returns a callable with the same signature that adds a behaviour: the same idea asLogging(inner). Usefunctools.wraps, or the wrapper hides the name and docstring (wrapperinstead offee), and it also records__wrapped__, which is how you reach the original. - But
@can do things that are not wrapping.@route("/fee")registers the function and returns the same function: nothing wraps it, and the last line proves it.@dataclassadds methods to a class, and@propertyreplaces a function with a different kind of object. They are all called decorators because of the syntax, not because of the pattern.
The differences that matter in a design:
| The Decorator pattern | Python's @decorator |
|
|---|---|---|
| What it is | A design pattern, in any language: a wrapper object with the same interface | Syntax, in Python only: @d above a definition means name = d(name) |
| What it wraps | An object, at run time, one instance at a time | A function or class, at definition time, for every caller |
| Choosing the stack | Per instance, from config or a request: the lesson's build(names) |
Fixed where the function is defined (apply it by hand to choose at run time) |
| What it may do | Add behaviour around a call | Anything the callable returns: wrap, register, replace, or modify a class |
You can write the pattern in Python without @, as the fee stack does, and use @ without writing the pattern, as @route does. In JavaScript the function-level version is a higher-order function (withRetry(fn)); decorator syntax is a separate matter that this lesson's programs don't use (they run on Node 20 with no flags). Java's @ annotations are metadata for tools, not wrappers.
Python's at sign is syntax that passes a name through a function. It is the pattern when that function returns a wrapper, not when it registers.
Decorator and its look-alikes#
| Pattern | What it does | How to tell it from Decorator |
|---|---|---|
| Proxy | Same interface, controls access to the real object (lazy, protection, caching, remote) | A decorator adds responsibilities and the client builds the stack. A proxy is often the one that creates and manages its subject |
| Adapter | Changes the interface | A decorator keeps it, which is what makes stacking possible |
| Strategy | Swaps how an object does its job | The book's phrase: a decorator changes an object's skin, a strategy changes its guts. A strategy replaces the rule, a decorator adds to whichever rule it wraps |
| Composite | Treats a group of parts as one part | Structurally close: a decorator is a composite with one child, but its point is adding behaviour |
| Chain of Responsibility | Passes a request along handlers, any of which may stop it | A decorator always forwards (it may retry or reshape the call, but it doesn't decide that nobody else handles it). Web middleware looks like both |
When not to use it#
- When there is one add-on, or two that never combine. A plain
ifis shorter. The pattern pays when combinations and order are open. - When you need to replace the behaviour, not add to it. That is a Strategy: the inner object's work is what you want to swap.
- When clients must know which object they hold. Type checks, equality and identity all see through to the wrong thing. Add the method to the interface instead.
- When the interface is wide. Forwarding forty methods in every wrapper is a bug farm. Narrow the interface (the interface-segregation lesson) or generate the forwarding.
- When nobody tests the order. The 210 and the 175 are both plausible. Put the order in one place and test the answer.
- When the stack gets deep. Each layer is another frame in a stack trace, and the call you are chasing is five wrappers down.
Where it shows up in the case studies#
| Case study | Class | How |
|---|---|---|
| Ride sharing | SurgeFare(inner, multiplier) |
"Decorator: any fare strategy, times the multiplier the rider agreed to." complete() builds SurgeFare(self.pricing, trip.surge).fare(km, minutes) from the multiplier stored when the trip was requested. The case study multiplies by a float and rounds; this lesson uses a whole-number 2 to keep money in whole rupees |
| Pub-sub logger | AsyncSink(inner) |
Wraps any slow Sink and is itself a Sink: callers enqueue and return, one worker writes, and a full queue drops and counts. It adds a close() and a dropped counter that Sink doesn't have, which is the "only the interface is forwarded" trap above, handled by keeping a reference to the wrapper |
| Parking lot | follow-up: free first 30 minutes | "A small decorator that wraps another pricing (FreeFirst30(HourlyPricing(...))). Pricing classes compose." The same shape as SmallCart |
In the interview#
Say it with the shape: "Decorator adds behaviour by wrapping an object in another object with the same interface, so wrappers stack and each add-on is a class instead of a subclass for every combination. The two things to watch are wrapper order and object identity."
Then expect the follow-ups:
- "Decorator or subclassing?" Subclassing is fixed at compile time and needs a class per combination: 8 for three add-ons, 16 once order counts. Decorators combine at run time.
- "Does the order matter?" Yes, and show it: the same three fee rules give 210 or 175. The outermost wrapper sees the call first and the result last.
- "Decorator or Proxy or Strategy?" A decorator adds responsibilities and the client stacks them, a proxy controls access to its subject, and a strategy changes the guts where a decorator changes the skin.
- "Is Python's
@decoratorthe Decorator pattern?" It is syntax forf = d(f). When it returns a wrapper with the same signature it is the pattern applied to a function; when it registers or modifies something it is not. - "What goes wrong with identity?" A wrapper isn't the object it wraps, so
isinstance,==andison a decorated object answer about the wrapper.
Common mistakes#
- Stacking without thinking about order, then being surprised that logging sees three attempts.
- Checking the concrete type of a decorated object (
isinstance,instanceof,dynamic_cast), or using it as a key without deciding what equality means. - Forgetting
functools.wraps, so every wrapped function is namedwrapperin logs and stack traces. - Calling every
@a Decorator pattern, or saying the pattern needs@at all. - A decorator that changes the interface (a new return type, a new argument). It is an Adapter then, and the stack no longer fits together.
Decorator
Q1/4 Three fee wrappers (small cart, late night, surge) are stacked in two different orders around the same 70-rupee base fee. The totals are 210 and 175. What explains the difference?
Answer & explanation
Answer: B
The outermost wrapper sees the call first and the result last. With surge outermost it doubles 105, with surge innermost it doubles only the 70. Neither order is a bug: it is a business rule, and the order should be visible and tested.
Q2/4 In which stack does a log line appear once per customer request, however many retries it took?
Answer & explanation
Answer: B
The outer wrapper sees the call once, and the retry hides the failed attempts from it. With logging inside the retry, it sees every attempt and writes one line each.
Q3/4 A client wrapped in a Retry decorator is checked with isinstance(x, FlakyClient) before use. What happens?
Answer & explanation
Answer: C
A decorator and its component aren't identical. The wrapper is a Client, so ask for the interface. Checking for the concrete class is a bug.
Q4/4 What is the relation between Python's @decorator syntax and the Decorator pattern?
Answer & explanation
Answer: C
@route returns the same function and @dataclass modifies a class: neither wraps anything. A function wrapper that keeps the signature and adds behaviour is the pattern, and so is a class like Retry that has no @ in it.
this lesson in shorts · 3
play them all →One idea from this lesson in about a minute each, made for your phone.
New lessons by email
New lessons, side quests and case files as they ship, at most once a week. No spam, unsubscribe anytime.
Discussion · …
Public: everyone reading this lesson sees comments. Be kind and keep it on the material.


