Decorator
What This Concept Is
Decorator is a structural pattern that attaches new behavior to an object by wrapping it in another object with the same interface. The wrapper adds work before or after calling the wrapped object, and because it implements the same interface, clients cannot tell the wrapper apart from the original.
The structure:
- a
Componentinterface - a
ConcreteComponentimplementing it - an abstract
Decoratorthat also implementsComponentand holds a reference to anotherComponent - concrete decorators that add their own behavior around the wrapped call
Decorators compose: Logger(Retry(Cache(Http()))) reads as a pipeline of layers, each adding one concern.
Why It Matters Here
Decorator is the pattern-based answer to the Open/Closed Principle for cross-cutting behavior: logging, retries, caching, authorization, rate limiting, metrics, input validation. Without Decorator, these concerns either leak into every class (smell: duplication) or hide inside subclass hierarchies that explode combinatorially.
Decorator is also one of the most language-sensitive patterns. In languages with first-class functions, many decorators collapse to a higher-order function. In strongly typed OO systems, the class-based form still wins because the wrapped object keeps its full interface.
Concrete Example
// Component
interface TextProcessor { process(text: string): string; }
// Concrete component
class PlainText implements TextProcessor { process(t: string) { return t; } }
// Abstract decorator
abstract class TextDecorator implements TextProcessor {
constructor(protected readonly wrapped: TextProcessor) {}
abstract process(t: string): string;
}
// Concrete decorators
class Trim extends TextDecorator { process(t: string) { return this.wrapped.process(t).trim(); } }
class Lowercase extends TextDecorator { process(t: string) { return this.wrapped.process(t).toLowerCase(); } }
class Normalize extends TextDecorator { process(t: string) { return this.wrapped.process(t).normalize('NFC'); } }
// Stack: each layer adds one transformation
const pipeline: TextProcessor =
new Lowercase(new Trim(new Normalize(new PlainText())));
console.log(pipeline.process(' Héllo WORLD ')); // "héllo world"
Structural sketch:
Client --> +--Decorator A--+
| +--Decorator B--+
| | +--Decorator C--+
| | | +---Component---+
| | | | (real work) |
| | | +---------------+
| | +----------------+
| +----------------+
+------------------+
Same interface at every layer. Order matters.
Common Confusion / Misconception
- Decorator vs Proxy. Both wrap. Decorator's intent is "add behavior." Proxy's intent is "control access, hide remoteness, or defer work." The structure is nearly identical; the intent -- and therefore the code -- differs.
- Decorator vs Subclass. Subclassing multiplies:
LoggedRetryingHttpClient,RetryingHttpClient,LoggedHttpClient. Decorator composes one concern per class and stacks them at runtime. - A Decorator that changes the interface is not a Decorator. That is an Adapter.
- Be mindful of object identity: wrapping breaks
==and sometimesequals. Caching, mocking, and equality-sensitive code care.
How To Use It
- Spot a cross-cutting concern that is currently duplicated across classes or inflated by a subclass hierarchy.
- Make sure the thing you want to wrap has (or can be given) an interface.
- Write one decorator per concern. Each should do one thing, in its own file.
- Compose at the composition root, not inside domain code.
- Keep decorators thin -- a decorator with real business logic is a service in disguise.
Check Yourself
- What concrete consequence does "Decorator keeps the component's interface" buy you?
- Why does Decorator work so naturally with dependency injection?
- When is a Decorator chain hurting readability, and what is the limit?
Mini Drill or Application
Build a decorator pipeline for a text processor: Trim, Normalize, Lowercase, CollapseWhitespace, RemovePunctuation. Do three things:
- Implement each decorator in isolation with unit tests.
- Assemble two different pipelines at the composition root.
- Prove that changing pipeline order changes output in at least one predictable way, and write a test that pins that property.