Published on

[Structural] Decorator Design Pattern

🎨 Decorator Design Pattern

The Decorator Pattern is a structural design pattern that lets you add new behavior to objects dynamicallywithout modifying their original class. It does this by wrapping objects in decorator classes that implement the same interface.


🧠 Core Idea (Intuition)

Instead of creating many subclasses like:

Coffee
 ├─ CoffeeWithMilk
 ├─ CoffeeWithSugar
 ├─ CoffeeWithMilkAndSugar
 ├─ CoffeeWithMilkSugarCaramel

You compose behavior at runtime:

CoffeeMilkDecoratorSugarDecoratorCaramelDecorator

This keeps classes small, flexible, and open for extension.


🧩 Structure

The pattern has four key roles:

  1. Component

    • Common interface for objects and decorators
  2. ConcreteComponent

    • The original object
  3. Decorator (abstract)

    • Wraps a Component
  4. ConcreteDecorator

    • Adds new behavior

✅ When to Use Decorator

Use it when:

  • You want to add responsibilities dynamically
  • Subclassing would cause a class explosion
  • You want to follow Open–Closed Principle
  • Behavior combinations should be flexible at runtime

Avoid it when:

  • You need strict object identity
  • Debugging layered behavior would be too complex

🐍 Python Example (Clean & Idiomatic)

1️⃣ Component Interface

from abc import ABC, abstractmethod

class Coffee(ABC):
    @abstractmethod
    def cost(self) -> float:
        pass

    @abstractmethod
    def description(self) -> str:
        pass

2️⃣ Concrete Component

class SimpleCoffee(Coffee):
    def cost(self) -> float:
        return 2.0

    def description(self) -> str:
        return "Simple coffee"

3️⃣ Base Decorator

class CoffeeDecorator(Coffee):
    def __init__(self, coffee: Coffee):
        self._coffee = coffee

    def cost(self) -> float:
        return self._coffee.cost()

    def description(self) -> str:
        return self._coffee.description()

4️⃣ Concrete Decorators

class MilkDecorator(CoffeeDecorator):
    def cost(self) -> float:
        return self._coffee.cost() + 0.5

    def description(self) -> str:
        return self._coffee.description() + ", milk"


class SugarDecorator(CoffeeDecorator):
    def cost(self) -> float:
        return self._coffee.cost() + 0.2

    def description(self) -> str:
        return self._coffee.description() + ", sugar"

5️⃣ Usage

coffee = SimpleCoffee()
coffee = MilkDecorator(coffee)
coffee = SugarDecorator(coffee)

print(coffee.description())  # Simple coffee, milk, sugar
print(coffee.cost())         # 2.7

🎯 Decorators stack cleanly and can be reordered or skipped.


🔍 Decorator vs Inheritance

AspectDecoratorInheritance
Runtime flexibility✅ High❌ Low
Class explosion❌ Avoided✅ Common
Open–Closed Principle✅ Yes❌ Often violated
Object identity❌ Wrapped✅ Preserved

⚙️ Real-World Examples

  • Java I/O Streams (BufferedInputStream)
  • GUI toolkits (scrollbars, borders)
  • Middleware pipelines
  • Python function decorators (@lru_cache, @retry)
  • Game buffs / debuffs (very common in RPG engines)

🧠 Mental Model

Inheritance adds behavior vertically. Decorator adds behavior horizontally.