1. The Problem
Problem: Add Responsibilities Without Modifying Existing Code
Sometimes you need to extend an object’s behavior (add features, capabilities, properties) without modifying the original class and without creating massive inheritance trees.
Example: beverages with optional condiments (Mocha, Whip, Soy, etc.)
Naive approach: subclass explosion.
EspressoWithMocha
EspressoWithMochaAndWhip
HouseBlendWithWhip
HouseBlendWithSoy
HouseBlendWithSoyAndMocha
Problems:
- Combinatorial explosion of subclasses
- Hard to mix and match features dynamically
- Changing or adding a condiment requires touching many classes
- Violates Open/Closed (you must modify existing code for new combinations)
We need to dynamically attach extra behavior to a single object at runtime without modifying existing classes.
2. The Decorator Pattern: Wrap Objects to Add Behavior Dynamically
Decorator pattern solves this by wrapping objects inside other objects.
Core idea:
- A base interface defines core behavior
- A decorator implements the same interface
- Decorators hold a reference to another object of the same interface
- Each decorator adds its own behavior and forwards the rest
Components:
- Component (Beverage) — the core interface
- Concrete Component (Espresso, HouseBlend) — basic objects
- Decorator (CondimentDecorator) — wrapper conforming to the same interface
- Concrete Decorators (Mocha, Whip) — add new behavior
Benefits:
- Add responsibilities at runtime
- Unlimited combinations through composition
- Open/Closed: add new decorators without touching existing code
- No subclass explosion
- Fine-grained, pluggable behavior layers
Use Decorator when:
- You need flexible, runtime-additive behavior
- You want to avoid huge inheritance hierarchies
- Combinations of features are arbitrary or unpredictable
- You want to stack multiple behaviors
3. Implementation: Beverage + Decorators in Python
Base Component
from abc import ABC, abstractmethod
class Beverage(ABC):
@abstractmethod
def get_description(self) -> str:
pass
@abstractmethod
def get_cost(self) -> float:
pass
Decorator Base
class CondimentDecorator(Beverage):
def __init__(self, beverage: Beverage) -> None:
self._beverage = beverage
@abstractmethod
def get_description(self) -> str:
pass
Concrete Beverages
class Espresso(Beverage):
def __init__(self) -> None:
self.description = "Espresso"
self.cost = 4.0
def get_description(self) -> str:
return self.description
def get_cost(self) -> float:
return self.cost
class HouseBlend(Beverage):
def __init__(self) -> None:
self.description = "House Blend"
self.cost = 5.0
def get_description(self) -> str:
return self.description
def get_cost(self) -> float:
return self.cost
Concrete Decorators
class Mocha(CondimentDecorator):
def __init__(self, beverage: Beverage) -> None:
super().__init__(beverage)
def get_description(self) -> str:
return self._beverage.get_description() + ", Mocha"
def get_cost(self) -> float:
return self._beverage.get_cost() + 1.0
class Whip(CondimentDecorator):
def __init__(self, beverage: Beverage) -> None:
super().__init__(beverage)
def get_description(self) -> str:
return self._beverage.get_description() + ", Whip"
def get_cost(self) -> float:
return self._beverage.get_cost() + 2.0
Usage
espresso = Espresso()
mocha_espresso = Mocha(espresso)
whip_mocha_espresso = Whip(mocha_espresso)
print(espresso.get_description(), ":", espresso.get_cost())
print(mocha_espresso.get_description(), ":", mocha_espresso.get_cost())
print(whip_mocha_espresso.get_description(), ":", whip_mocha_espresso.get_cost())
drink = Mocha(Whip(Mocha(HouseBlend())))
print(drink.get_description(), ":", drink.get_cost())
4. When to Use Decorator (and When Not To)
Use Decorator when:
- You need dynamic, runtime-extensible features
- Behavior must be layered or stacked
- You want fine-grained control over feature composition
- You want to avoid rigid inheritance trees
- Adding a feature should not modify existing classes
Avoid Decorator when:
- Object identity must remain simple (deeply nested decorators obscure structure)
- A fixed set of combinations is known (direct subclasses might be simpler)
- Debugging complexity matters (many nested wrappers make tracing harder)
- State must be shared across layers in a complex way