Learning Objectives
- Understand the Decorator pattern concept
- Add functionality without inheritance
- Use function decorators and wrappers
- Implement class decorators
- Apply decorator best practices
What is the Decorator Pattern?
Decorators wrap objects to add new behavior without modifying the original object's code. They provide a flexible alternative to subclassing for extending functionality.
Function Decorator
function logger(fn) {
return function(...args) {
console.log(`Calling ${fn.name} with`, args);
const result = fn(...args);
console.log(`Result:`, result);
return result;
};
}
function add(a, b) {
return a + b;
}
const loggedAdd = logger(add);
loggedAdd(2, 3);
// Calling add with [2, 3]
// Result: 5
// Returns: 5
Memoization Decorator
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log('From cache');
return cache.get(key);
}
console.log('Computing...');
const result = fn(...args);
cache.set(key, result);
return result;
};
}
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
const memoizedFib = memoize(fibonacci);
console.log(memoizedFib(10)); // Computing... 55
console.log(memoizedFib(10)); // From cache 55
Real-World Examples
API Retry Decorator
function retry(fn, retries = 3, delay = 1000) {
return async function(...args) {
for (let i = 0; i < retries; i++) {
try {
return await fn(...args);
} catch (error) {
console.log(`Attempt ${i + 1} failed`);
if (i === retries - 1) {
throw error;
}
await new Promise(resolve =>
setTimeout(resolve, delay * (i + 1))
);
}
}
};
}
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error('Fetch failed');
return response.json();
}
const fetchUserWithRetry = retry(fetchUser, 3, 1000);
try {
const user = await fetchUserWithRetry(1);
} catch (error) {
console.error('All retries failed');
}
Timing Decorator
function timing(fn) {
return async function(...args) {
const start = performance.now();
const result = await fn(...args);
const end = performance.now();
console.log(`${fn.name} took ${(end - start).toFixed(2)}ms`);
return result;
};
}
async function processData(data) {
await new Promise(r => setTimeout(r, 1000));
return data.map(x => x * 2);
}
const timedProcess = timing(processData);
await timedProcess([1, 2, 3]);
// processData took 1002.34ms
Authorization Decorator
function requireAuth(fn) {
return function(...args) {
const user = getCurrentUser();
if (!user) {
throw new Error('Authentication required');
}
return fn.call(this, ...args);
};
}
function requireRole(role) {
return function(fn) {
return function(...args) {
const user = getCurrentUser();
if (!user || !user.roles.includes(role)) {
throw new Error(`Role ${role} required`);
}
return fn.call(this, ...args);
};
};
}
class UserService {
deleteUser(id) {
console.log(`Deleting user ${id}`);
}
}
UserService.prototype.deleteUser = requireAuth(
requireRole('admin')(UserService.prototype.deleteUser)
);
Rate Limiting Decorator
function rateLimit(fn, limit, window) {
const calls = [];
return function(...args) {
const now = Date.now();
// Remove old calls outside window
while (calls.length && calls[0] < now - window) {
calls.shift();
}
if (calls.length >= limit) {
throw new Error('Rate limit exceeded');
}
calls.push(now);
return fn(...args);
};
}
function sendEmail(to, subject) {
console.log(`Sending email to ${to}`);
}
const limitedSendEmail = rateLimit(sendEmail, 5, 60000); // 5 per minute
// Can call 5 times
for (let i = 0; i < 5; i++) {
limitedSendEmail('user@example.com', 'Hello');
}
// 6th call throws error
limitedSendEmail('user@example.com', 'Hello'); // Error: Rate limit exceeded
Composing Decorators
function compose(...decorators) {
return function(fn) {
return decorators.reduceRight((decorated, decorator) => {
return decorator(decorated);
}, fn);
};
}
// Compose multiple decorators
const enhance = compose(
logger,
timing,
memoize,
retry
);
async function fetchData(url) {
const response = await fetch(url);
return response.json();
}
const enhancedFetch = enhance(fetchData);
When to Use Decorator Pattern
Good use cases:
- Add logging/monitoring to functions
- Caching and memoization
- Input validation
- Authorization and authentication
- Rate limiting and throttling
- Error handling and retry logic
- Performance measurement
Benefits
- Open/Closed Principle: Extend without modifying
- Single Responsibility: Each decorator has one job
- Composable: Combine multiple decorators
- Flexible: Add/remove at runtime
- Reusable: Use across different functions
Best Practices
1. Preserve function signature
function decorator(fn) {
return function(...args) {
// Preserve original behavior
return fn(...args);
};
}
2. Use descriptive names
// Good
const loggedFetch = logger(fetch);
const cachedFetch = memoize(fetch);
// Not as clear
const f1 = logger(fetch);
const f2 = memoize(fetch);
3. Keep decorators focused
// Good - single responsibility
function logger(fn) { /* just logging */ }
function timer(fn) { /* just timing */ }
// Bad - doing too much
function loggerAndTimer(fn) { /* logging and timing */ }
Key Takeaways
- Decorators add behavior without modifying original code
- Wrap functions/objects to extend functionality
- Composable and reusable across codebase
- Alternative to inheritance for extending behavior
- Great for cross-cutting concerns (logging, caching, auth)
- Follow Open/Closed and Single Responsibility principles
- Can be composed for powerful combinations
- Preserve original function signatures