Concepts / Structural patterns / Decorator

Decorator

Adding behaviour by wrapping, without subclass explosion.

01The intuition

The problem it solves

A coffee shop app prices drinks. A coffee can have milk, extra shots, syrup and whipped cream, in any combination. The first version tries to make a class per combination.

class Coffee:
    def cost(self): return 100
class MilkCoffee:
    def cost(self): return 120
class MilkSyrupCoffee:
    def cost(self): return 140
class MilkSyrupCreamCoffee:
    def cost(self): return 160
class SyrupCoffee:
    def cost(self): return 120
# ... 2 to the power of 4 classes, and we have not added a second shot yet

Four add-ons give sixteen classes; a fifth add-on doubles that. The alternative people reach for is one Coffee class with a boolean per add-on, which means every new add-on edits Coffee and the cost method becomes a ladder of ifs.

A decorator is an object that wraps another object of the same interface and adds one thing to it. Because the wrapper has the same interface, you can wrap a wrapper, and combinations come for free.

Build it one piece at a time

Step 1: define the interface and one plain implementation.

from abc import ABC, abstractmethod

class Drink(ABC):
    @abstractmethod
    def cost(self) -> int: ...
    @abstractmethod
    def label(self) -> str: ...

class Coffee(Drink):
    def cost(self) -> int: return 100
    def label(self) -> str: return "coffee"

Step 2: write one wrapper that holds a Drink and is a Drink. It adds its own price to whatever it wraps.

class Milk(Drink):
    def __init__(self, inner: Drink): self._inner = inner
    def cost(self) -> int: return self._inner.cost() + 20
    def label(self) -> str: return self._inner.label() + " + milk"

print(Milk(Coffee()).cost())          # 120

Step 3: add more wrappers, never more combinations. Each add-on is one small class. Stacking them is composition at the call site.

class Syrup(Drink):
    def __init__(self, inner: Drink): self._inner = inner
    def cost(self) -> int: return self._inner.cost() + 20
    def label(self) -> str: return self._inner.label() + " + syrup"

class ExtraShot(Drink):
    def __init__(self, inner: Drink): self._inner = inner
    def cost(self) -> int: return self._inner.cost() + 30
    def label(self) -> str: return self._inner.label() + " + shot"

order = ExtraShot(Syrup(Milk(Coffee())))
print(order.label(), order.cost())    # coffee + milk + syrup + shot 170

Step 4: use the same shape for behaviour, not just numbers. A decorator can add logging, caching or a size multiplier around any drink, and the drink does not know.

class Large(Drink):
    def __init__(self, inner: Drink): self._inner = inner
    def cost(self) -> int: return int(self._inner.cost() * 1.5)
    def label(self) -> str: return "large " + self._inner.label()
StepClasses for 4 add-onsAdding a 5th add-onMilk twice
Start1616 more classesa 17th class
Boolean flags1edit Coffee and its cost laddera counter field
2 and 351 new classMilk(Milk(coffee))

Why it works

The trick is that the wrapper and the wrapped share an interface, so to the caller a decorated drink is just a drink. Each decorator owns exactly one responsibility and adds it by delegating to the inner object and adjusting the result. The number of classes grows with the number of add-ons, not with the number of combinations, and new add-ons never touch existing classes, which is the open-closed principle in action.

Where it breaks: order-dependent decorators (Large(Syrup(c)) and Syrup(Large(c)) price differently), so either document the order or make the multiplier a different mechanism; and identity checks, because isinstance(order, Coffee) is now false. Debugging a stack of six wrappers is also harder than reading one class, so give each a clear label or repr.

The trade-off you must say out loud

Decorators trade a flat, readable object for a chain you must unwind in your head. They are right when add-ons combine freely and independently; they are wrong when add-ons interact, where a single class with explicit rules is clearer. Say the test: "each add-on changes the result in a way that does not depend on the others". The follow-up is often "how do I remove milk later", and the answer is that decorators are built, not edited; rebuild the chain or keep a list of add-on choices and construct from it.

Where it shows up in the 100 Google problems

02Used in these problems