Design Patterns (GoF and modern) Module 10 – Anti-patterns and refactoring to patterns
Refactoring toward, and away from, patterns
Refactor toward Strategy and Decorator in small steps, away from a needless Singleton or Visitor, and keep every answer the same at each step.
What you will learn
- Refactor toward Strategy, State, Decorator, Composite, Observer and Null Object in small steps
- Refactor away from patterns that no longer pay for themselves, such as a needless Singleton or Visitor
- Prove with tests that behaviour stays identical at every step
Before you start
What you’ll learn · Before you start
What you will learn
- Refactor toward Strategy, State, Decorator, Composite, Observer and Null Object in small steps
- Refactor away from patterns that no longer pay for themselves, such as a needless Singleton or Visitor
- Prove with tests that behaviour stays identical at every step
Before you start
On this page
Refactoring toward a pattern means moving existing code, one small step at a time and with its behaviour unchanged, until the pattern’s structure appears. Refactoring away from a pattern means taking that structure out again once the variation it served has gone. Joshua Kerievsky’s Refactoring to Patterns treats patterns this way, as destinations that code reaches when it asks for them, and its catalogue of 27 refactorings gives advice for refactoring “to, towards or away from” them. The discipline is the same in both directions: name the variation, keep the tests green after every step, and commit each step so it can be undone alone. Patterns are rarely right on the first day of a design, and rarely right forever.
The refactorings in both directions
Most of the names below are Kerievsky’s; the rest come from Martin Fowler’s catalogue of refactorings or from a language feature:
Toward a pattern
| Refactoring | What it does |
|---|---|
| Replace Conditional Logic With Strategy | Moves each branch of a conditional into a strategy object that the code using it is given. |
| Replace State-Altering Conditionals with State | Moves the transitions and each status’s behaviour into one class per state. |
| Move Embellishment To Decorator | Moves optional extras out of the core code into wrappers put together once, at set-up. |
| Replace Implicit Tree With Composite | Turns nested lists or strings that form a tree into node objects with one interface. |
| Replace Hard-Coded Notifications With Observer | Replaces direct calls to interested code with listeners registered from outside. |
| Introduce Null Object | Replaces repeated checks for a missing object with an object that does nothing harmful. |
Away from a pattern
| Refactoring | What it does |
|---|---|
| Inline Singleton | Creates the one instance at start-up and passes it to whoever needs it. |
| Inline Function, for a factory with one product | Calls the constructor directly where the factory only ever made one class. |
| A sealed type and a switch, instead of a Visitor | Lets a compiler check every case of a closed hierarchy, without double dispatch. |
| Remove Middle Man, for a mediator that only forwards | Lets callers talk to the real object when the layer between adds no decision. |
Refactoring toward a pattern starts from a variation that has become real, and the same steps run backwards when it disappears:
Toward a pattern, and away from it
Text description of the diagram
The diagram has three boxes and two directions.
- Plain code, such as a conditional, flags or a direct call, leads to a box "Small steps, tests green after each" through an arrow labelled "toward: the variation becomes real".
- The small steps lead to a box "A pattern", such as Strategy, Decorator, State or Observer.
- A dashed arrow labelled "away: the variation is gone" leads from the pattern back to plain code.
Refactoring away from patterns starts with a map of what depends on them. This scanner reads a small app with
Python’s ast module and finds everything that reaches a Singleton, and the factory behind it:
Python · reach/reach.py
"""Before removing a Singleton, find everything that reaches it, and every factory that only ever makes one class."""
import ast
from pathlib import Path
tree = ast.parse(Path("notify_app.py").read_text())
callers = []
for owner in [None, *[n for n in tree.body if isinstance(n, ast.ClassDef)]]:
body = tree.body if owner is None else owner.body
for f in (n for n in body if isinstance(n, ast.FunctionDef)):
for call in ast.walk(f):
if isinstance(call, ast.Call) and isinstance(call.func, ast.Attribute) and call.func.attr == "instance":
callers.append(f"{owner.name}.{f.name}" if owner else f.name)
break
print(f"Notifications.instance() is called from {len(callers)} places: {', '.join(callers)}")
for cls in (n for n in tree.body if isinstance(n, ast.ClassDef)):
for f in (n for n in cls.body if isinstance(n, ast.FunctionDef)):
returns = [r.value for r in ast.walk(f) if isinstance(r, ast.Return) and isinstance(r.value, ast.Call)]
made = {ast.unparse(r.func) for r in returns}
if f.name == "create" and len(made) == 1:
print(f"{cls.name}.create() only ever makes {made.pop()}: a factory with one product") Output
Notifications.instance() is called from 5 places: welcome, password_reset, order_shipped, Reminders.due, Reminders.overdue NotifierFactory.create() only ever makes EmailNotifier: a factory with one product
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 reach.py
reach/notify_app.py
"""A small app whose messages go through a Singleton made by a factory with one product."""
class EmailNotifier:
def send(self, to, text):
return f"email to {to}: {text}"
class NotifierFactory:
@staticmethod
def create():
return EmailNotifier()
class Notifications:
_instance = None
@classmethod
def instance(cls):
if cls._instance is None:
cls._instance = cls()
cls._instance.notifier = NotifierFactory.create()
return cls._instance
def send(self, to, text):
return self.notifier.send(to, text)
def welcome(user):
return Notifications.instance().send(user, "welcome aboard")
def password_reset(user):
return Notifications.instance().send(user, "here is your reset link")
def order_shipped(user, order):
return Notifications.instance().send(user, f"order {order} has shipped")
class Reminders:
def due(self, user):
return Notifications.instance().send(user, "your invoice is due")
def overdue(self, user):
return Notifications.instance().send(user, "your invoice is overdue") Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Five places call Notifications.instance(), two of them in another class, and the factory only ever makes
EmailNotifier. Those five call sites are the work list of an Inline Singleton, and the factory can go in the same
series of small steps.
Toward Strategy: a ride fare in four steps Pro: opens with a Pro, Premium or Ultimate pass
In this part:
- Toward Decorator: the optional extras of a gift order
- Away from a Singleton: tests that pass in one order only
- Away from Visitor: a closed set of shapes
- Choosing the direction from evidence
- The first and the last step of four more refactorings
- At system scale: the Strangler Fig
- Practise: remove a needless Singleton and its factory
- Remove a needless Singleton and its one-product factory
- Check yourself
- Interview questions
- What does it mean to refactor toward a pattern, and when would you refactor away from one?
- A test suite passes on developers' machines but fails in CI about one run in five, always in tests that send notifications through a Singleton. Explain the likely cause, and plan the refactoring away from the Singleton without one big change.
- You are moving three pricing flags, gift wrap, insurance and express delivery, into decorators. Insurance is a percentage of the total. What can go wrong with the order of the decorators, and how do you prove that the refactoring changed no price?
- A Java 25 code base has a Visitor over six AST node types with nine visitors. A colleague proposes replacing it with sealed interfaces and switch expressions. What do you weigh, and how would you migrate safely if you agree?
- Key takeaways
4 runnable examples (JavaScript, MissingCase.java, Python, SealedShapes.java, VisitorShapes.java, step0.mjs, step0.py, step1.mjs, step1.py, step2.mjs, step2.py, step3.mjs, step3.py) · 1 exercise with sample tests · 3 interview answers · about 21 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
References
- Refactoring to Patterns (Joshua Kerievsky (Industrial Logic))
- Refactoring to Patterns catalog (Industrial Logic)
- Catalog of Refactorings (Martin Fowler)
- JEP 441: Pattern Matching for switch (OpenJDK)
- Strangler Fig (Martin Fowler)
- importlib: the implementation of import (Python Software Foundation)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress