Skip to content

Refactoring to Patterns

Arriving at a pattern from real pain instead of starting with one — the smells that point to each pattern, and the safe sequence of moves.

Advanced18 minUpdated 8 August 2026

Every topic in this module has warned against applying its pattern too early. This one is about the alternative: letting the code tell you which pattern it needs, and getting there in steps that are individually safe.

The premise is that a pattern applied speculatively is a guess about what will vary, and a pattern arrived at from repeated pain is a response to evidence. The second is nearly always better, and the difference is not effort — it is timing.

Smells and what they point to

A code smell is a surface symptom. It does not tell you the answer, but it narrows the search considerably.

SmellLikely pattern
A growing conditional over interchangeable behavioursStrategy
Conditionals on a status field, repeated across methodsState
Construction logic duplicated, or callers choosing concrete typesFactory
A constructor with many optional parametersBuilder or named arguments
Subclasses multiplying across two independent axesBridge
One class per combination of orthogonal concernsDecorator
The same sequence of calls repeated by several callersFacade or Template Method
A third party's vocabulary appearing throughout your codeAdapter
One event with several unrelated reactionsObserver
A chain of if checks that each reject a requestChain of Responsibility
Adding an operation means editing every type in a closed setVisitor
Long parameter lists of primitives passed togetherValue object first, then look again

The last row is the most valuable and the most skipped. Many apparent pattern problems dissolve once related primitives become a type — the conditional was branching on a string that should have been an enum, and half the branches vanish.

The sequence that makes it safe

Conceptual diagramoften noyesNotice repeated painWrite characterisation testsfor current behaviourDo the boring cleanups:rename, extract, introduce typesIs the patternstill needed?Stop — the smell wasthe mess, not the designApply the patternin small stepsDelete the old path
Diagram, as text
  • Notice repeated pain leads to Write characterisation tests for current behaviour
  • Write characterisation tests for current behaviour leads to Do the boring cleanups: rename, extract, introduce types
  • Do the boring cleanups: rename, extract, introduce types leads to Is the pattern still needed?
  • Is the pattern still needed? leads to Stop — the smell was the mess, not the design (often no)
  • Is the pattern still needed? leads to Apply the pattern in small steps (yes)
  • Apply the pattern in small steps leads to Delete the old path

Characterisation tests come first, always. You are changing code whose behaviour nobody has written down, so the current behaviour — including its bugs — is the specification. Without these tests, a refactor is a rewrite with optimism.

Then do the boring things, and give them their own commits: rename to reveal intent, extract functions, replace primitives with types, remove dead branches. This step routinely makes the pattern unnecessary, which is the cheapest possible outcome and the reason not to skip ahead.

Then apply the pattern in small steps, each of which leaves the code working. A refactor that only compiles at the end cannot be reviewed, bisected or abandoned halfway.

Then delete the old path. The step that gets dropped, leaving both implementations and all the maintenance of each.

A worked sequence: conditional to Strategy

The most common refactor in this module, in the order the steps should happen:

  1. Characterise. Tests covering every branch, including the default. If a branch has no test you cannot verify it survived.
  2. Extract each branch into a named function. Behaviour unchanged, and the names alone often reveal that two branches are the same thing.
  3. Give the functions a uniform signature. This is where you discover whether they are genuinely interchangeable. If one needs an extra parameter, that is real information — Strategy may not fit.
  4. Replace the conditional with a lookup from key to function. The conditional is now one line, and in many languages this is where you stop.
  5. Only if needed, introduce an interface and classes — when a strategy needs state, several related operations, or metadata such as a display name.
  6. Delete the old conditional and its tests, keeping the characterisation tests that assert behaviour.

Step 3 is where refactors fail informatively. Discovering that the branches are not interchangeable is a successful outcome, not a failed refactor — it has told you the pattern does not fit before you committed to it.

Knowing when to stop

Stop when the pain stops. The reason to refactor was a specific difficulty — adding a variant, testing a rule, understanding a flow. When that is gone, further improvement is speculative.

Stop when the next step needs a guess. If you cannot name the axis of variation from existing code, you would be inventing one.

Stop when the pattern is bigger than the problem. Two branches that have not changed in three years do not need Strategy, however cleanly it would apply.

Check your understanding

Common mistakes

Refactoring without characterisation tests. A rewrite with optimism, and no signal when it breaks.

Mixing refactoring and behaviour change in one commit. Neither can be verified, and the whole thing gets reverted.

Skipping the boring cleanups. They often remove the need for the pattern entirely.

A refactor that only works at the end. It cannot be reviewed, bisected or abandoned safely.

Forcing the pattern when the branches turn out not to be interchangeable. That discovery was the point.

Not deleting the old path. Two implementations to maintain, forever.

Refactoring speculatively rather than what you are touching. It loses to feature work and misses the code that matters.

Continuing past the point where the pain stopped. Further improvement is a guess.

Practice

A 400-line function computes shipping cost with nested conditionals over country, weight and carrier. There are no tests. Give the sequence of changes you would make, and identify the point at which you might decide no pattern is needed.

Write your own answer first, on paper or in a note. Then compare.

Show a hint

The last question is the important one — plan for the answer being 'stop'.

Show a strong answer

Step one: characterisation tests, generated rather than hand-written. Four hundred lines of nested conditionals has too many paths to enumerate by hand. I would drive the function across a grid — every country, weights either side of each band boundary, every carrier — record the outputs, and lock them in as the specification. Boundary values matter more than midpoints, because that is where off-by-one errors live and where a refactor is most likely to shift behaviour.

Step two: coverage check. Any branch the grid never reaches is either dead or reachable only by inputs I have not thought of. Both need resolving before touching anything — an unexercised branch cannot be verified to have survived.

Step three: the boring cleanups, each its own commit.

  • Replace magic strings and numbers with named constants or enums. Countries and carriers as strings compared against literals is almost certainly part of why this is 400 lines.
  • Introduce value types — Weight, Money, Country. This is the step that most often collapses the mess, because half the nesting may be unit conversion and validation that the types now handle.
  • Extract the deepest nested blocks into named functions. The names alone will reveal duplicated logic.
  • Merge branches that turn out to be identical. There will be some.

Step four: reassess — and this is the decision point the question asks about.

After the cleanups, the function may be 120 lines of clear code with three named helpers. If adding a carrier is now a small, obvious change and the code is readable, I stop. No pattern. The smell was the mess, not the design, and every pattern I add from here is permanent indirection bought with no evidence.

The specific signals that would tell me to stop: the remaining conditional is short and flat; each branch is a call to a named function; nobody has to read the whole thing to change one carrier.

If I do continue, the structure I would expect — as covered under Strategy — is a strategy per carrier, weight bands as data rather than code, and country handled as carrier availability plus a separate tax step. Three different kinds of variation, not one.

Step five, whichever way it goes: delete the old code and any tests that were testing the old structure, keeping the characterisation tests that assert behaviour. Those stay permanently — they are now the specification that never existed.

What I would resist throughout: fixing bugs I find along the way in the same commits. A wrong rate for one country is a behaviour change, needs a decision from someone who owns pricing, and must not be mixed into a refactor.

A colleague proposes refactoring a well-understood, rarely-changed 300-line module to use three patterns, arguing it will be cleaner. There are no current complaints about it. Respond.

Write your own answer first, on paper or in a note. Then compare.

Show a hint

Argue from evidence and cost, and give them something concrete rather than a flat no.

Show a strong answer

Start from what refactoring is for. It is a response to difficulty — adding a variant is hard, a rule cannot be tested, a flow cannot be understood. None of those has been claimed here. "Cleaner" is an aesthetic judgement, and aesthetic judgements are not shared, so a change justified only by taste will be reversed by the next person with different taste.

The costs, stated concretely rather than as a principle:

  • Refactoring well-understood code trades a known state for an unknown one. The current version has been running without complaint, which is real information about its correctness that a rewritten version does not have.
  • Every abstraction is permanent. Three patterns is three pieces of indirection that every future reader follows, forever, in a module that is rarely read because it rarely changes.
  • The characterisation tests do not exist. Doing this safely means writing them first, which is most of the work — for a module nobody is struggling with.
  • The opportunity cost is real. There are almost certainly modules in this codebase that people do struggle with, and that is where the same effort would pay.

The question I would ask, and it is genuinely a question: what change did you try to make that was harder than it should have been? If there is an answer — "I needed to add a variant and had to touch four places" — then there is evidence, the conversation changes completely, and I would support a targeted refactor addressing that specific difficulty. If there is no answer, the proposal is looking for a problem.

What I would offer instead, so this is not simply a refusal:

  • Improve the naming and add a short comment explaining the module's job. Cheap, reversible, and it addresses "hard to read" if that is the real complaint underneath.
  • Add characterisation tests now, independently. Genuinely valuable — it makes any future change safe, and it is useful whether or not a refactor ever happens. This is the version of their instinct I would actively encourage.
  • Write down what they would change and why, and revisit when the module next needs modifying. If the prediction turns out right, the refactor is then justified by evidence and takes ten minutes to agree.

How I would frame the disagreement: not "this is over-engineering", which invites a defence of the patterns, but "we would be spending our refactoring budget on the module that is causing us the least trouble". That is a prioritisation argument rather than a taste argument, and it is much easier to agree on.

Key takeaways

  • Arrive at a pattern from repeated pain, not from the catalogue. Speculative patterns are guesses about what will vary.
  • Smells narrow the search but do not give the answer — and the most valuable move is often replacing primitives with types first.
  • Characterisation tests come first, always. Current behaviour, bugs included, is the only specification that exists.
  • Do the boring cleanups next. They frequently make the pattern unnecessary, which is the cheapest outcome.
  • Never mix refactoring with behaviour change in one commit, and never leave a refactor that only works at the end.
  • In the conditional-to-Strategy sequence, uniform signatures is the pivotal step — discovering the branches are not interchangeable is a success.
  • Stop when the pain stops, when the next step needs a guess, or when the pattern is bigger than the problem.
  • Refactor what you are already changing. Dedicated refactoring time loses to feature work and misses the code that matters.

Further reading

  • Joshua Kerievsky, Refactoring to Patterns (Addison-Wesley, 2004).
  • Martin Fowler, Refactoring, second edition (Addison-Wesley, 2018) — the catalogue of smells and mechanical refactorings.
  • Michael Feathers, Working Effectively with Legacy Code (Prentice Hall, 2004) — characterisation tests and seams.
  • Kent Beck, Tidy First? (O'Reilly, 2023) — small structural changes and when to make them.
  • Sandi Metz, Practical Object-Oriented Design in Ruby (Addison-Wesley, 2012).

Check yourself

3 questions in this lesson. Answer each once to complete it.

Practice this

Next lessonChoosing Patterns: A Worked Example
Lessons in this track