Patterns as a Refactoring Destination, Not a Starting Point
'Refactoring to patterns' inverts the usual way patterns are taught: instead of picking a pattern up front and designing around it, you let code smells drive small refactorings, and a recognizable pattern emerges as a byproduct once enough smells are resolved. This matters because premature pattern application — imposing a Strategy or Visitor before the code has demonstrated the variation that justifies it — adds indirection with no payoff, which is itself a form of over-engineering. The discipline is to keep the simplest thing that works until a specific smell (duplicated conditional logic, a switch statement that keeps growing, a class instantiating concrete types directly) signals that a pattern will pay for its complexity.
Cricket analogy: A young batter shouldn't be taught the reverse-scoop shot on day one; the shot 'emerges' as a refactoring only after the coach sees a specific recurring problem — being cramped by yorkers at the death — that the reverse-scoop specifically solves, not as a starting template.
From Switch Statement to Strategy: A Worked Smell
The most common refactoring-to-patterns trigger is a switch statement (or long if/else chain) on a type code that keeps growing every time a new variant is added, and that same switch statement is duplicated in two or three other methods across the codebase. The refactoring path is: first apply Extract Method to isolate each branch's logic into its own named method, then apply Replace Conditional with Polymorphism (or, in languages without inheritance-friendly types, Replace Type Code with Strategy objects) so each variant becomes its own class implementing a shared interface, and the switch collapses into a single polymorphic call or a lookup in a registry map. The payoff is that adding a new variant becomes 'add one new class,' not 'find and update every switch statement across the codebase.'
Cricket analogy: A switch statement that keeps growing is like an umpire's mental checklist for every possible dismissal type, hardcoded in his head; refactoring to Strategy is like each dismissal type (LBW, caught, run-out) becoming its own specialist review protocol that DRS applies polymorphically rather than the umpire re-deriving rules from scratch each time.
# BEFORE: growing switch/if-elif duplicated in shipping and pricing modules
def calculate_shipping_cost(order):
if order.method == "standard":
return order.weight * 0.5
elif order.method == "express":
return order.weight * 1.5 + 5
elif order.method == "overnight":
return order.weight * 3.0 + 15
# every new method requires editing this AND estimate_delivery_days()
# AFTER: Replace Conditional with Polymorphism (Strategy)
class ShippingMethod:
def cost(self, order): raise NotImplementedError
def delivery_days(self, order): raise NotImplementedError
class Standard(ShippingMethod):
def cost(self, order): return order.weight * 0.5
def delivery_days(self, order): return 5
class Express(ShippingMethod):
def cost(self, order): return order.weight * 1.5 + 5
def delivery_days(self, order): return 2
class Overnight(ShippingMethod):
def cost(self, order): return order.weight * 3.0 + 15
def delivery_days(self, order): return 1
SHIPPING_METHODS = {"standard": Standard(), "express": Express(), "overnight": Overnight()}
def calculate_shipping_cost(order):
return SHIPPING_METHODS[order.method].cost(order)
# Adding "international" now means one new class, not two edited functionsKerievsky's rule of thumb: 'refactor to a pattern the third time a smell shows up, not the first.' Two occurrences might be coincidence; a third occurrence of the same duplicated conditional is strong evidence the variation is a real, recurring axis worth abstracting.
Keeping Refactorings Small and Reversible
The safest way to refactor toward a pattern is a sequence of tiny, individually compilable, individually testable steps — never a single large leap from 'switch statement' to 'finished Strategy hierarchy' in one commit. A typical sequence is: extract the first branch's body into a private method with a descriptive name, repeat for each branch, introduce an interface that all the extracted methods' signatures share, wrap each branch in a class implementing that interface, replace the conditional with a map lookup or polymorphic dispatch, and only then delete the original conditional. At every step the test suite should stay green, which means each step can be committed independently and reverted independently if it turns out to be wrong.
Cricket analogy: A team rebuilding its batting order doesn't swap five positions in one match; they test one change (moving the opener to three) for a full series, confirm it works, then make the next single change — exactly like committing one refactoring step at a time.
Beware 'pattern-itis' during a refactor: once you start seeing Strategy everywhere, it's tempting to abstract every remaining conditional preemptively. Stop refactoring once the actual smell is resolved — don't extend the pattern to hypothetical future variants that haven't appeared yet (YAGNI).
- Refactoring to patterns lets a pattern emerge from smells rather than being imposed upfront.
- Premature pattern application adds indirection without payoff and is itself a form of over-engineering.
- A growing, duplicated switch statement on a type code is the classic trigger for Replace Conditional with Polymorphism.
- Kerievsky's rule of thumb: refactor to a pattern on the third occurrence of a smell, not the first.
- Refactor in small, individually testable, individually revertible steps — never one large leap.
- Keep the test suite green after every single small step so regressions are traceable to one change.
- Stop once the actual smell is resolved; avoid extending patterns to hypothetical future variants (YAGNI).
Practice what you learned
1. According to the 'refactoring to patterns' philosophy, when should a design pattern typically be introduced?
2. What code smell most classically triggers a Replace Conditional with Polymorphism refactor?
3. What is Kerievsky's 'rule of three' as applied to refactoring toward a pattern?
4. Why should refactoring steps toward a pattern be kept small and independently testable?
5. What is 'pattern-itis' as warned about in the context of refactoring to patterns?
Was this page helpful?
You May Also Like
Anti-Patterns to Avoid
A tour of the most common design anti-patterns — recurring bad solutions to recurring problems — and how to recognize and refactor them before they calcify.
Design Patterns in Modern Frameworks
How classic GoF design patterns show up, sometimes disguised under different names, inside React, Spring, Angular, and other modern frameworks.
Design Patterns Quick Reference
A condensed, scan-friendly cheat sheet mapping each core Gang of Four pattern to its intent, the force that triggers it, and a one-line example.
Related Reading
Related Study Notes in Software Engineering
Browse all study notesMicroservices Study Notes
Software Architecture · 30 topics
Software EngineeringTesting & TDD Study Notes
Software Testing · 30 topics
Software EngineeringSoftware Engineering Study Notes
Python · 40 topics
Software EngineeringGit & Version Control Study Notes
Bash · 40 topics
Software EngineeringSystem Design Study Notes
Architecture · 40 topics