Skip to content

Decorator Pattern - Dynamically add extra behavior

Updated: at 12:00 AM

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:

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:

Components:

Benefits:

Use Decorator when:

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:

Avoid Decorator when: