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

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