Lesson 11 / 25
Decorator, Function Decorators and Middleware
Add behaviour by wrapping.
Same interface, extra behaviour
Intent: attach additional responsibilities to an object dynamically by wrapping it in another object with the same interface. Decorators can be stacked: retries around logging around the real HTTP client. This avoids a subclass explosion (LoggingRetryingCachingClient). The same idea appears in functional form as higher-order functions (a function that takes a function and returns an enhanced one), in Python's @decorator syntax, in TypeScript decorators (check your TypeScript version and configuration, since the standard and legacy "experimental" decorators differ), and in middleware stacks in web frameworks. Use it for cross-cutting concerns: logging, caching, retries, metrics, authorisation. Watch out for deep stacks that make stack traces and ordering hard to follow.
A retry decorator as a higher-order function
Wraps any async function with the same signature.
type AsyncFn<A extends unknown[], R> = (...args: A) => Promise<R>;
function withRetry<A extends unknown[], R>(fn: AsyncFn<A, R>, attempts = 3, delayMs = 200): AsyncFn<A, R> {
return async (...args: A) => {
let lastError: unknown;
for (let i = 0; i < attempts; i++) {
try {
return await fn(...args);
} catch (err) {
lastError = err;
await new Promise((r) => setTimeout(r, delayMs * 2 ** i));
}
}
throw lastError;
};
}
function withTiming<A extends unknown[], R>(name: string, fn: AsyncFn<A, R>): AsyncFn<A, R> {
return async (...args: A) => {
const start = performance.now();
try { return await fn(...args); }
finally { metrics.record(name, performance.now() - start); }
};
}
// Stack decorators: timing wraps retries wraps the real call
const fetchExchangeRate = withTiming('fx.fetch', withRetry(rawFetchExchangeRate));Order matters
Timing outside retries measures total time including retries; timing inside measures each attempt. Decide deliberately, and document the order of middleware.
Quick check: What distinguishes a Decorator from an Adapter?
- They are identical
- A Decorator always uses inheritance
- An Adapter adds caching
- A Decorator keeps the same interface and adds behaviour; an Adapter changes the interface
Answer
A Decorator keeps the same interface and adds behaviour; an Adapter changes the interface — Decorators enhance; adapters translate.