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
- 2. Structure
- 3. C++ Example
- 4. Compile-time Alternative
- 5. When to Use
- 6. Variants and Pitfalls
- 7. Related Patterns
- 8. References
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 == coreis 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_ptrownership 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.