Decorator
You'll learn to
- -Stack behavior onto an object at runtime without subclass explosion, using a coffee/pizza-style worked example
- -Explain why Decorator beats inheritance when a combinatorial number of feature combinations is possible
Decorator attaches additional responsibilities to an object dynamically, providing a flexible alternative to subclassing for extending behavior. It is the pattern that directly answers the composition-vs-inheritance chapter from earlier: whenever a feature set could combine in many ways, wrapping beats subclassing every combination.
The Coffee Shop Example, Worked Through
A coffee ordering system needs to support any combination of milk, extra shots, and syrup on top of a base coffee, each adding its own cost. Modeling this with subclasses (CoffeeWithMilk, CoffeeWithMilkAndExtraShot, CoffeeWithMilkAndSyrupAndExtraShot...) requires one class per combination - it explodes combinatorially. Decorator instead wraps a Coffee in any number of decorators, each adding its own cost and description on top of whatever it wraps.
class Coffee(ABC):
@abstractmethod
def cost(self) -> float: ...
@abstractmethod
def description(self) -> str: ...
class SimpleCoffee(Coffee):
def cost(self) -> float: return 2.00
def description(self) -> str: return "Coffee"
class CoffeeDecorator(Coffee): # base decorator - same interface as Coffee
def __init__(self, coffee: Coffee):
self._coffee = coffee
class MilkDecorator(CoffeeDecorator):
def cost(self) -> float: return self._coffee.cost() + 0.50
def description(self) -> str: return self._coffee.description() + " + Milk"
class ExtraShotDecorator(CoffeeDecorator):
def cost(self) -> float: return self._coffee.cost() + 0.75
def description(self) -> str: return self._coffee.description() + " + Extra Shot"
# Any combination, at runtime, with no new class needed per combination:
order = ExtraShotDecorator(MilkDecorator(SimpleCoffee()))
print(order.description(), order.cost()) # "Coffee + Milk + Extra Shot" 3.25Why This Beats Inheritance Here
The key structural fact: every decorator implements the exact same interface (Coffee) as the thing it wraps, and holds a reference to a Coffee - which could be a SimpleCoffee or another decorator. This is what makes stacking possible: MilkDecorator does not need to know whether it is wrapping a plain coffee or a coffee that already has three other decorators applied, because from its perspective, both are equally just "a Coffee." With N independent add-ons, subclassing needs up to 2^N classes; Decorator needs exactly N decorator classes, combined at runtime in any order.
A well-known real-world example of this exact pattern: Java and Python's I/O stream classes (BufferedReader wrapping a FileReader, GZIPInputStream wrapping a FileInputStream) are Decorators - each layer adds behavior while presenting the same stream interface.
Interview Signal is part of Pro
See a real weak answer next to a real strong one for this exact topic.
Quiz is part of Pro
Test what you just read with a short quiz, and bank the XP.