Decorator Pattern


  • Description: Attach responsibilities to an object dynamically by wrapping it in an object with the same interface — composition-based alternative to subclassing for behavior extension.
  • My Notion Note ID: K2C-2-9
  • Created: 2026-05-22
  • Updated: 2026-05-22
  • License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io

Table of Contents


1. Why It Exists

  • Subclassing for every feature combo → combinatorial explosion. BufferedEncryptedLoggedStream, EncryptedBufferedStream, etc.
  • Decorator → each feature is its own wrapper, stacked at runtime.
  • Same interface as the wrapped object → clients can't tell the difference.

Canonical example → java.io streams (BufferedInputStream(new GZIPInputStream(new FileInputStream(...)))). Equivalent idea in <iostream> — chained stream buffers, filters.


2. Structure

Role Responsibility
Component Common interface for both real objects and decorators.
ConcreteComponent The "core" object being decorated.
Decorator (base) Implements Component, holds a Component* (the wrappee), forwards calls by default.
ConcreteDecorator Extends Decorator, overrides operations to add behavior before/after delegating to the wrappee.

Recursive composition → D1(D2(D3(Core))). Each layer adds one slice of behavior.


3. C++ Example

3.1 Stream-style decorators

#include <memory>
#include <string>
#include <string_view>
#include <iostream>
#include <utility>

// Component
class TextStream {
public:
    virtual ~TextStream() = default;
    virtual void write(std::string_view s) = 0;
};

// ConcreteComponent
class ConsoleStream : public TextStream {
public:
    void write(std::string_view s) override { std::cout << s; }
};

// Decorator base
class StreamDecorator : public TextStream {
public:
    explicit StreamDecorator(std::unique_ptr<TextStream> inner)
        : inner_(std::move(inner)) {}
    void write(std::string_view s) override { inner_->write(s); }
protected:
    std::unique_ptr<TextStream> inner_;
};

// ConcreteDecorators
class UpperCase : public StreamDecorator {
public:
    using StreamDecorator::StreamDecorator;
    void write(std::string_view s) override {
        std::string up(s);
        for (auto& c : up) c = static_cast<char>(std::toupper(c));
        inner_->write(up);
    }
};

class Bracketed : public StreamDecorator {
public:
    using StreamDecorator::StreamDecorator;
    void write(std::string_view s) override {
        inner_->write("[");
        inner_->write(s);
        inner_->write("]");
    }
};

int main() {
    std::unique_ptr<TextStream> s = std::make_unique<ConsoleStream>();
    s = std::make_unique<UpperCase>(std::move(s));
    s = std::make_unique<Bracketed>(std::move(s));
    s->write("hello");          // → [HELLO]
    std::cout << "\n";
}
  • Order matters → Bracketed(UpperCase(Console))UpperCase(Bracketed(Console)).
  • Each decorator owns its inner by unique_ptr → strict, single-owner chain.

4. Compile-time Alternative

When decoration is fixed at compile time, templates eliminate virtual dispatch.

4.1 Mixin chain via templates

template <class Base>
class UpperCaseMixin : public Base {
public:
    using Base::Base;
    void write(std::string_view s) {
        std::string up(s);
        for (auto& c : up) c = static_cast<char>(std::toupper(c));
        Base::write(up);
    }
};

template <class Base>
class BracketedMixin : public Base {
public:
    using Base::Base;
    void write(std::string_view s) {
        Base::write("[");
        Base::write(s);
        Base::write("]");
    }
};

struct ConsoleBase {
    void write(std::string_view s) { std::cout << s; }
};

using Stream = BracketedMixin<UpperCaseMixin<ConsoleBase>>;
// Stream s; s.write("hi");  → [HI]
  • No vtable, no heap allocation, inlining-friendly.
  • Decoration order fixed at type-construction time.

4.2 CRTP variant

CRTP works when each decorator needs static_cast access to the most-derived type — useful when wrappers depend on each other's static interfaces. Plain mixin chains are simpler when only Base::method forwarding is needed.

Trade-off: compile-time decoration loses runtime flexibility (can't add a feature based on a config flag), and template-instantiation cost grows with chain depth.


5. When to Use

Use when:

  • Want to add/remove responsibilities at runtime, transparently to clients.
  • Subclassing every feature combination would blow up.
  • Behavior should be composable in arbitrary order (logging, compression, encryption, retries).
  • Cross-cutting concerns sit naturally between client and core (caching, instrumentation).

Avoid when:

  • Decoration order is fixed → templates / mixins are cheaper.
  • Adding a decorator changes the interface → that's an Adapter, not a Decorator.
  • Single feature, never combined → just subclass or inline the call.

6. Variants and Pitfalls

  • Transparent vs interface-extending. Strict Decorator preserves the interface. Extending the interface (adding new methods) breaks the substitutability that makes the pattern work — clients now downcast.
  • Skin vs Guts. Decorator changes "skin" (what wraps the object); Strategy changes "guts" (algorithm inside). Use Strategy when you want a behavior swap, not a wrapping stack.
  • Order sensitivity. Encryption-then-compression ≠ compression-then-encryption. Document the intended order.
  • Identity loss. decorated == core is false. Equality, hashing, downcasts on the wrapped object misbehave.
  • Deep chains, deep traces. A 6-layer decorator chain in a stack trace looks like noise. Hurts debugging.
  • Allocation pressure. Heap-allocated wrappers per request → allocator hotspot in tight loops.
  • Lifetime. Wrappee must outlive wrapper. unique_ptr ownership transfer keeps this honest.

7. Related Patterns

  • Decorator vs Adapter. Adapter changes the interface. Decorator preserves the interface and adds behavior. Asking "did the caller's expected interface stay the same?" disambiguates.
  • Decorator vs Proxy. Proxy controls access (lazy, remote, security) — same interface, often opaque or "you didn't ask for this". Decorator adds responsibilities — same interface, behavior is the explicit point. Mechanically similar (wrap and forward) but intent differs: Proxy guards, Decorator enriches.
  • Decorator vs Facade. Facade introduces a new, simpler interface over many classes. Decorator wraps one object while keeping its interface.
  • Decorator vs Composite. Composite aggregates many children; Decorator wraps one. Decorator can be seen as a degenerate Composite with exactly one child whose primary job is augmentation.
  • Decorator vs Chain of Responsibility. Both build a chain of wrappers. CoR's chain can short-circuit (a handler stops the chain). Decorator's chain always forwards (modifies on the way).
  • Strategy. Use Strategy if you want to vary the algorithm; Decorator if you want to add behavior around an algorithm.

8. References

  • GoF — Design Patterns, Decorator ch.
  • refactoring.guru — Decorator
  • sourcemaking — Decorator
  • Alexandrescu — Modern C++ Design, policy-based design / mixin chains
  • cppreference — CRTP