Skip to content

Decorator Pattern — Add Behavior Dynamically

DodaTech Updated 2026-06-24 3 min read

What You'll Learn

You will learn how the Decorator pattern wraps objects with new behaviour in a composable, stackable fashion, offering a flexible alternative to subclassing. You will understand when to reach for Decorator versus static inheritance.

Why It Matters

Subclassing leads to an explosion of classes — one for every combination of features. A coffee shop with three beverage types and four condiments would need 12 classes. With Decorator, you compose behaviour at runtime, adding only one class per condiment. The combinatorial problem grows exponentially: ten features would require hundreds of subclasses to cover every combination. Decorator reduces this to ten classes, one per feature, composed as needed.

Real-World Use

Stream APIs in programming languages chain filters, buffers, and compression layers around raw I/O streams — BufferedInputStream(GZIPInputStream(FileInputStream("file.gz"))). DodaTech's middleware pipeline uses decorators to layer authentication, logging, rate-limiting, and Caching around HTTP handlers without touching the core handler. Adding a new middleware simply means writing one class and inserting it into the pipeline — no modifications to existing handlers.

The Pattern

Component defines the interface. ConcreteComponent is the core object. Decorator maintains a reference to a Component and adds its own behaviour before or after delegating.

from abc import ABC, abstractmethod

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

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

class Espresso(Beverage):
    def cost(self) -> float:
        return 2.50

    def description(self) -> str:
        return "Espresso"

class CondimentDecorator(Beverage):
    def __init__(self, beverage: Beverage):
        self._beverage = beverage

class Milk(CondimentDecorator):
    def cost(self) -> float:
        return self._beverage.cost() + 0.50

    def description(self) -> str:
        return self._beverage.description() + ", Milk"

class Whip(CondimentDecorator):
    def cost(self) -> float:
        return self._beverage.cost() + 0.30

    def description(self) -> str:
        return self._beverage.description() + ", Whip"

class Caramel(CondimentDecorator):
    def cost(self) -> float:
        return self._beverage.cost() + 0.75

    def description(self) -> str:
        return self._beverage.description() + ", Caramel"
drink = Espresso()
drink = Milk(drink)
drink = Whip(drink)
drink = Caramel(drink)
print(f"{drink.description()} — ${drink.cost():.2f}")
Espresso, Milk, Whip, Caramel — $4.05

Structure

classDiagram
    class Component {
        <>
        +operation()
    }
    class ConcreteComponent {
        +operation()
    }
    class Decorator {
        #component: Component
        +operation()
    }
    class ConcreteDecoratorA {
        +operation()
    }
    class ConcreteDecoratorB {
        +operation()
    }
    ConcreteComponent ..|> Component
    Decorator ..|> Component
    ConcreteDecoratorA --|> Decorator
    ConcreteDecoratorB --|> Decorator
    Decorator --> Component : delegates

Real-World Usage

  • Python @decorator syntax — functions can be wrapped with behaviour like @staticmethod, @lru_cache, or custom decorators.
  • Java I/O Streamsnew BufferedInputStream(new GZIPInputStream(new FileInputStream("file.gz"))) layers behaviour.
  • .NET Middleware PipelineASP.NET Core uses app.Use() to stack middleware components around the request handler.
  • Ruby on Rails before_action — interceptors that add cross-cutting behaviour to controller methods.
  • Adapter changes the interface; Decorator preserves it while adding behaviour.
  • Composite aggregates children; Decorator wraps a single component.
  • Strategy swaps algorithms; Decorator layers responsibilities.
  • Proxy controls access; Decorator adds responsibility.

Pros and Cons

Pros Cons
More flexible than static inheritance Many small classes increase codebase size
Behaviour can be composed at runtime Order of wrapping matters and can cause subtle bugs
Follows the Open/Closed Principle Debugging a heavily decorated object is harder
Single Responsibility Principle — each decorator does one thing Decorator and component types must match exactly

Notice in the code that each decorator both wraps and conforms to the Beverage interface. This is the defining characteristic of the Decorator pattern — the wrapper is transparent to the client. The drink variable, after being wrapped three times, is still a Beverage and can be used anywhere a Beverage is expected.

Practice Questions

  1. How would you implement a logging decorator that records method entry and exit with timestamps?
  2. Compare Decorator with subclassing — when does the tipping point favour one over the other? Consider a UI component with optional borders, scrollbars, and shadows.
  3. Implement a decorator that caches the results of a computationally expensive operation, invalidating the cache when the underlying component changes.
  4. What challenges arise when a decorator needs to access the concrete component's specialised methods that aren't part of the common interface?

Challenge

Implement a decorator that measures and reports execution time for any operation. Chain it with existing decorators and verify that the timing wraps the entire decorated call stack, not just the innermost operation.

Real-World Task

Look at your application's HTTP middleware stack. Map each middleware to a Decorator class and use DodaTech's middleware profiler to measure the performance contribution of each layer in the pipeline.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro