Decorator
Attach new behaviours to objects at runtime by wrapping them in decorator objects. A flexible alternative to subclassing — stack multiple wrappers to compose behaviour.
The Problem
You have a Notifier that sends email alerts. Now users want SMS alerts too. And Slack. And some users want email + SMS. Others want all three. With subclassing you’d need: EmailNotifier, SMSNotifier, SlackNotifier, EmailAndSMSNotifier, EmailAndSlackNotifier, SMSAndSlackNotifier, AllThreeNotifier — seven classes for three channels. Add a fourth and it doubles again.
Subclassing for behaviour combinations doesn’t scale. You need a way to add behaviour at runtime without modifying the original class.
The Solution
Wrappers. Each decorator implements the same interface as the component it wraps, but adds behaviour before or after delegating to the wrapped object. Stack them like layers — each layer adds something new.
Real-World Analogy
Wearing clothes. You put on a base layer (t-shirt), add a mid-layer (fleece), add an outer layer (rain jacket). Each layer adds thermal/weather properties without removing the layers below. You can swap layers independently. Taking off the rain jacket doesn’t un-wear the fleece.
TypeScript Example
// Component interface
interface DataSource {
writeData(data: string): void;
readData(): string;
}
// Concrete component — the thing being decorated
class FileDataSource implements DataSource {
private data = '';
constructor(private readonly filename: string) {}
writeData(data: string): void {
console.log(`[File] Writing to ${this.filename}`);
this.data = data;
}
readData(): string {
return this.data;
}
}
// Base decorator — delegates everything to wrapped component
abstract class DataSourceDecorator implements DataSource {
constructor(protected wrapped: DataSource) {}
writeData(data: string): void {
this.wrapped.writeData(data);
}
readData(): string {
return this.wrapped.readData();
}
}
// Concrete decorators — each adds one responsibility
class EncryptionDecorator extends DataSourceDecorator {
writeData(data: string): void {
const encrypted = Buffer.from(data).toString('base64'); // simplified encryption
console.log('[Encrypt] Encrypting data');
super.writeData(encrypted);
}
readData(): string {
const data = super.readData();
console.log('[Encrypt] Decrypting data');
return Buffer.from(data, 'base64').toString('utf-8');
}
}
class CompressionDecorator extends DataSourceDecorator {
writeData(data: string): void {
// Simplified: just annotate it
const compressed = `COMPRESSED(${data})`;
console.log('[Compress] Compressing data');
super.writeData(compressed);
}
readData(): string {
const data = super.readData();
console.log('[Compress] Decompressing data');
return data.replace(/^COMPRESSED\(/, '').replace(/\)$/, '');
}
}
class LoggingDecorator extends DataSourceDecorator {
private log: string[] = [];
writeData(data: string): void {
this.log.push(`write at ${new Date().toISOString()}`);
super.writeData(data);
}
getLog(): string[] { return this.log; }
}
// Compose decorators — order matters
const plain = new FileDataSource('data.txt');
// Layered: logging → compression → encryption → file
const logging = new LoggingDecorator(plain);
const compressed = new CompressionDecorator(logging);
const encrypted = new EncryptionDecorator(compressed);
encrypted.writeData('Hello, Design Patterns!');
/*
[Encrypt] Encrypting data
[Compress] Compressing data
[File] Writing to data.txt
*/
const result = encrypted.readData();
/*
[Encrypt] Decrypting data
[Compress] Decompressing data
*/
console.log(result); // → Hello, Design Patterns!
console.log(logging.getLog()); // → ['write at 2026-...']
When to Use
- You need to add behaviours to objects at runtime without affecting other objects
- You have behaviour combinations that are impractical to cover with subclassing
- You want to compose behaviours in different orders or stacks for different contexts
- Examples: I/O streams (Java), middleware pipelines, logging/caching wrappers, permission checks
Pros
- Extend object behaviour without subclassing
- Combine multiple decorators to compose complex behaviour
- Add/remove responsibilities at runtime
- Single Responsibility — each decorator handles one concern
Cons
- Can produce many small objects — hard to debug when stacked deeply
- Order matters — incorrect stacking causes bugs that are hard to trace
- Removing a specific decorator from the middle of a stack is awkward
Related Patterns
- Composite organises a tree of components; Decorator adds behaviour to a single component
- Adapter changes the interface; Decorator keeps the same interface and adds behaviour
- Chain of Responsibility passes a request through a chain; Decorator wraps components — similar structure, different intent
- Proxy controls access and has the same interface; Decorator adds behaviour