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.