Refactoring toward patterns
Design patterns are destinations you refactor TOWARD when a smell demands them, not templates you impose up front. Duplicated construction pulls toward a Factory; a growing conditional toward Strategy or State. YAGNI stops you cargo-culting all 23 GoF patterns into a simple app.
You learned the Gang-of-Four catalogue and now you can see all 23 patterns everywhere. So you reach for them up front: the new feature gets a Strategy and a Factory and a Builder before a single duplicate has appeared. Six months later the codebase is a maze of indirection where every one-line change requires tracing through four files — and most of the flexibility you built for never got used.
There’s a different way to arrive at patterns, due to Joshua Kerievsky: patterns are destinations you refactor toward when the code asks for them, not blueprints you start from. A smell appears, you remove it, and the shape you land on happens to be a named pattern. The pattern is the direction the smell pulls you, and YAGNI is the brake that stops you driving there before the smell exists.
After this lesson you can explain why a design pattern is the end state of a refactoring rather than a starting template; recognise the specific smells that pull code toward a Factory, a Strategy, or a State machine; and apply the senior discipline of refactoring to patterns instead of prematurely toward them — letting the smell justify the pattern and letting YAGNI veto patterns no smell has earned.
A pattern is a destination, not a starting point. The Gang-of-Four catalogue describes recurring solutions; it does not tell you when the problem is present. Kerievsky’s reframing — “refactor to patterns” — treats each pattern as the target of a series of small, behaviour-preserving refactorings, applied only once the code exhibits the smell that pattern dissolves. You don’t decide “this needs a Strategy” in a design meeting; you notice a conditional that keeps growing along one axis, and that observation is what makes Strategy the right move. The pattern earns its place by removing a concrete pain, not by appearing in a textbook.
The practical consequence: you never write interface PaymentStrategy on day one. You write the simplest thing, and you let the second and third payment method show you the duplication before you extract the abstraction.
Duplicated construction logic pulls you toward a Factory. When the same multi-step object-creation appears in several call sites — and especially when the type being created depends on a runtime value — the smell is duplicated, branching construction. The refactoring that removes it lands you on a factory.
// SMELL: the same "which class?" decision is duplicated at every call site
const a = type === "pdf" ? new PdfReport(data) : new CsvReport(data);
// ...elsewhere, again:
const b = kind === "pdf" ? new PdfReport(other) : new CsvReport(other);The decision “which Report subclass” is copy-pasted. Centralise it:
function createReport(type: string, data: Data): Report {
if (type === "pdf") return new PdfReport(data);
return new CsvReport(data);
}
const a = createReport(type, data);
const b = createReport(kind, other);Now a new format is one edit inside createReport; callers depend on the concept “make a report”, not on the concrete classes. You didn’t plan a Factory — you removed duplication and a Factory is what the duplication-removal produced.
A conditional that grows along one axis pulls you toward Strategy. A switch or if-chain that keeps gaining branches for the same reason — a new shipping method, a new pricing rule, a new export format — is the “switch statements” smell. Each branch is an interchangeable algorithm wedged into one function. Strategy is where removing that smell takes you: pull each branch into its own object behind a shared interface, and the conditional collapses into a lookup.
// SMELL: one function that grows a branch every time a method is added
function shippingCost(method: string, weight: number): number {
if (method === "standard") return weight * 1.5;
if (method === "express") return weight * 1.5 + 10;
if (method === "overnight") return weight * 3 + 25;
throw new Error("unknown method");
}Refactor toward Strategy — but only once the third or fourth branch has proven the axis is real:
type ShippingStrategy = (weight: number) => number;
const strategies: Record<string, ShippingStrategy> = {
standard: (w) => w * 1.5,
express: (w) => w * 1.5 + 10,
overnight: (w) => w * 3 + 25,
};
function shippingCost(method: string, weight: number): number {
const strategy = strategies[method];
if (!strategy) throw new Error("unknown method");
return strategy(weight);
}Adding a method is now an entry in the map, not a branch in a growing function — and unrelated methods stop sharing a code path you have to re-test.
A conditional that branches on a lifecycle phase pulls you toward State. Strategy’s cousin: when the conditional isn’t choosing an algorithm but checking “what mode am I in right now?” — and transitions between modes are scattered as this.status = "..." assignments — the pull is toward the State pattern. The tell is a status field that several methods all branch on, with the same if (status === ...) ladder repeated.
// SMELL: every method re-derives behaviour from a status string
class Order {
status = "draft";
submit() { if (this.status !== "draft") throw Error("can't submit"); this.status = "submitted"; }
pay() { if (this.status !== "submitted") throw Error("can't pay"); this.status = "paid"; }
cancel() { if (this.status === "paid") throw Error("can't cancel"); this.status = "cancelled"; }
}State extracts each phase into its own object that owns its legal transitions, so an illegal transition is impossible by construction rather than guarded by a chain of string checks. But notice the cost: State is heavier than Strategy. For three states you might keep the explicit checks; the pattern earns itself only when the number of states and transitions makes the scattered ladder genuinely hard to reason about. The smell — duplicated if (status === ...) across many methods — is the gate.
Let the smell, not the catalogue, decide. A teammate opens a PR adding a second notification channel. The current code is one function:
// BEFORE — works, one channel, zero smell yet
function notify(user: User, message: string) {
sendEmail(user.email, message);
}The naive “pattern-first” instinct is to introduce a NotificationStrategy interface, a NotifierFactory, and a registry — for one channel. That’s pattern-itis: indirection with no duplication to justify it. YAGNI vetoes it.
Now the second channel arrives, and a conditional appears:
function notify(user: User, message: string, channel: string) {
if (channel === "email") sendEmail(user.email, message);
else if (channel === "sms") sendSms(user.phone, message);
}Two branches is still borderline — but a third (push) lands, the axis is now clearly “channel”, and the conditional is the “switch statements” smell. Now you refactor toward Strategy, because the smell has earned it:
type Notifier = (user: User, message: string) => void;
const notifiers: Record<string, Notifier> = {
email: (u, m) => sendEmail(u.email, m),
sms: (u, m) => sendSms(u.phone, m),
push: (u, m) => sendPush(u.deviceId, m),
};
function notify(user: User, message: string, channel: string) {
const send = notifiers[channel];
if (!send) throw new Error(`unknown channel: ${channel}`);
send(user, message);
}Same destination a pattern-first developer would have reached on day one — but reached on day three, after the duplication proved the abstraction was needed. The two extra days bought you certainty about the right axis to abstract: had the real variation turned out to be per-user templating rather than per-channel transport, the day-one Strategy would have abstracted the wrong axis and you’d be unwinding it. Refactoring to the pattern let the code tell you which axis was real before you committed.
▸Why this works
Why wait for the smell instead of designing the pattern in? Because a pattern is a bet on which change is coming, and bets made before evidence are usually wrong. Every pattern adds indirection — more files, more names, more hops to trace during the next read. That cost is justified only when it buys flexibility along an axis that actually varies. Refactoring to a pattern means you’ve already seen the variation (the second and third case), so you’re abstracting the axis the code demonstrably changes along, not a hypothetical one. Kerievsky’s whole point is that this ordering — smell first, pattern second — gets you the right abstraction far more reliably than up-front design, and it keeps the simple cases simple.
▸Common mistake
The signature failure mode is pattern-itis: wrapping a one-line need in a Strategy + Factory + Builder when a plain function would do. It usually follows learning the GoF catalogue — every problem suddenly looks like it needs a pattern, and “flexible” becomes a synonym for “good”. It isn’t. A Factory that creates exactly one type, a Strategy interface with one implementation, a Builder for an object with two fields — these are all indirection with no duplication removed and no variation absorbed. The smell that justifies the pattern is absent, so the pattern is pure cost. The senior tell: if you can’t point at the concrete duplication or growing conditional the pattern dissolves, you don’t have a pattern — you have cargo-cult ceremony, and a plain function is the better design.
A developer adding the first export format introduces an ExportStrategy interface, an ExporterFactory, and a registry — for a single CSV exporter. By the 'refactor to patterns' discipline, what's the correct assessment?
Design patterns are destinations you refactor toward, not templates you impose up front (Kerievsky). A smell points the direction: duplicated construction pulls toward a Factory, a conditional growing along one algorithm-axis pulls toward Strategy, and a status-field ladder repeated across methods pulls toward State. The discipline is refactor to patterns, not prematurely toward them — the smell justifies the pattern, and YAGNI vetoes any pattern no smell has earned, stopping you from cargo-culting all 23 GoF patterns into a simple app. The failure mode is pattern-itis: wrapping a one-line need in Strategy + Factory + Builder when a plain function would do. The test, as always in this track: can you name the concrete smell this pattern dissolves? If not, the plain function is the better code.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.