Decorator Pattern

Intent
“Attach new behaviors to objects dynamically by placing them inside wrapper objects that contain the behavior.” — Gang of Four (GoF)
Problem
Ts
class Notifier {
send(message: string): void {
// send email
}
}
- SMS notifications
- Facebook messages
- Slack alerts
Solution
- All decorators implement the same interface.
- They contain a reference to the next component in the chain.
- Each decorator can augment behavior before or after delegating.
TypeScript Example – Notification Stack
Common Interface
Ts
interface Notifier {
send(message: string): void;
}
Base Component
Ts
class EmailNotifier implements Notifier {
send(message: string): void {
console.log(`Email: ${message}`);
}
}
Base Decorator
Ts
abstract class NotifierDecorator implements Notifier {
constructor(protected wrappee: Notifier) {}
send(message: string): void {
this.wrappee.send(message);
}
}
SMS Decorator
Ts
class SMSNotifier extends NotifierDecorator {
send(message: string): void {
super.send(message);
console.log(`SMS: ${message}`);
}
}
Slack Decorator
Ts
class SlackNotifier extends NotifierDecorator {
send(message: string): void {
super.send(message);
console.log(`Slack: ${message}`);
}
}
Client Code
Ts
const baseNotifier = new EmailNotifier();
const smsNotifier = new SMSNotifier(baseNotifier);
const fullNotifier = new SlackNotifier(smsNotifier);
fullNotifier.send("🚨 Alert: Server down!");
Output
Email: 🚨 Alert: Server down!
SMS: 🚨 Alert: Server down!
Slack: 🚨 Alert: Server down!
When to Use
- You need to add responsibilities dynamically.
- You want to avoid subclassing for each feature combination.
- You want to follow the Open/Closed Principle.
Pros and Cons
Pros
- Behavior added at runtime without modifying classes.
- Combine behaviors flexibly via composition.
- Follows Single Responsibility Principle: split logic into modular decorators.
Cons
- Hard to debug or track deeply nested decorators.
- The order of decorators can affect behavior.
- Initial configuration can be complex and verbose.