BuildBot

Patterns That Matter

Strategy & dependency injection

Lesson 1 of 5

What you'll learn

  • See how the Strategy pattern collapses into a first-class function in TypeScript.
  • Understand dependency injection as nothing more than passing dependencies as arguments.
  • Build a provider-agnostic router that selects a handler at call time.

Strategy is just a function

The classic Strategy pattern wants an interface, a family of classes implementing it, and a context object that holds a reference to the current strategy. In a language with first-class functions, that whole hierarchy is a function parameter.

// GoF Strategy: an interface and a class per strategy.
interface PricingStrategy {
  price(cents: number): number;
}
class HolidayPricing implements PricingStrategy {
  price(cents: number) {
    return Math.round(cents * 0.8);
  }
}

// Modern equivalent: the strategy is the function itself.
type Pricing = (cents: number) => number;
const holiday: Pricing = (cents) => Math.round(cents * 0.8);
const checkout = (cents: number, pricing: Pricing) => pricing(cents);

No context class, no registry, no ceremony. You swap behavior by passing a different function. The type (cents: number) => number documents the contract more clearly than an interface with one method ever did.

Dependency injection without a container

"Dependency injection" sounds like it needs a framework and decorators. It does not. DI is the idea that a unit of code should receive its collaborators instead of constructing them. In TypeScript, that means accepting them as arguments.

type Logger = { info: (msg: string) => void };
type Clock = { now: () => number };

// Dependencies arrive as arguments — trivially swappable in tests.
function createOrder(deps: { logger: Logger; clock: Clock }) {
  return (id: string) => {
    deps.logger.info(`order ${id} at ${deps.clock.now()}`);
  };
}

Strategy and DI are the same move at different scales: pass behavior in rather than hard-coding it. A provider-agnostic router is the two ideas combined — a lookup table of handler functions, selected by a key at call time.

Skip the container

Reach for a DI container only when you have deep, dynamic dependency graphs. For most app code, passing a plain deps object is clearer, faster to read, and infinitely easier to mock.

Provider-agnostic router

Run the router. It picks a handler strategy by provider name and applies it to the request. Notice that adding a provider means adding a function, not a subclass.

Loading editor…
Knowledge check

In modern TypeScript, what does the Strategy pattern usually collapse into?

Saved on this device. Sign in to sync your progress everywhere.