Refactoring Safely
Change a program's structure without changing what it does — in small, tested steps you can stop between at any time.
Big idea
Refactoring is changing code's structure without changing its behaviour, in steps so small that each one is obviously safe. The discipline is the point: tests green → one small move → tests green. Rewrites gamble; refactoring compounds.
The problem it solves
Everyone has seen the "big cleanup" branch: two weeks of moving things around, 4,000 changed lines, and a merge that breaks three features nobody can trace. Or the opposite: code nobody dares touch, so every new feature is bolted on the side and the mess grows. Refactoring is the middle path — improve the design continuously, in moves small enough to review, test, and ship the same day.
Think of it like
Rearranging a furnished room while living in it. You don't empty the house and rebuild; you move one bookcase, check you can still reach the door, then move the next. At every point the room is usable, and you can stop when it's good enough.
The loop
- Have a safety net. Tests that pin current behaviour — even rough "characterisation tests" that just record today's output.
- Name the smell (Bloaters, Couplers, …) and the refactoring that addresses it.
- Take the smallest step: rename one thing, extract one function, move one method.
- Run the tests. Green: commit. Red: undo — don't debug a refactoring.
- Never mix refactoring with behaviour changes in one commit. "Make the change easy, then make the easy change."
In code: characterisation tests
When old code has no tests, record what it does now, then refactor until the recordings still match.
def legacy_grade(s): # nobody remembers why it's written like this
g = ""
if s >= 90: g = "A"
if s < 90 and s >= 80: g = "B"
if s < 80 and s >= 70: g = "C"
if s < 70: g = "F"
if s > 100 or s < 0: g = "?"
return g
def grade(score): # refactored: guard clause, ordered thresholds
if not 0 <= score <= 100:
return "?"
for floor, letter in ((90, "A"), (80, "B"), (70, "C")):
if score >= floor:
return letter
return "F"
inputs = [-5, 0, 69, 70, 79, 80, 89, 90, 100, 101]
recorded = {s: legacy_grade(s) for s in inputs} # the characterisation
mismatches = [s for s in inputs if grade(s) != recorded[s]]
print("recorded:", "".join(recorded[s] for s in inputs))
print("refactor:", "".join(grade(s) for s in inputs))
print("mismatches:", mismatches or "none")output
recorded: ?FFCCBBAA?
refactor: ?FFCCBBAA?
mismatches: noneRun and edit it in the app.
Pick inputs at the edges (69/70, 100/101) — that's where restructured conditions usually go wrong.
Which change is a refactoring?
Put a safe refactoring step in order.
Watch it
When to refactor — and when not to
- Before a change, to make it easy ("preparatory refactoring").
- While reading, to make code say what you just figured out ("comprehension refactoring").
- After it works, to clean up before you move on.
Don't refactor code you're about to delete, code with no safety net you can't build, or right before a deadline on a path you won't touch again. And don't call a rewrite a refactoring.
Pros and cons
| Pros | Cons |
|---|---|
| Design improves continuously, at low risk | Needs tests (or the discipline to write some) |
| Every step is reviewable and shippable | Small steps feel slow at first |
| Understanding goes into the code, not a wiki | Easy to drift into behaviour changes |
How it relates
- The code smells say where; the technique lessons say how: Composing Methods, Moving Features, Organizing Data, Simplifying Conditionals, Method Calls, Generalization.
- Refactoring is what makes YAGNI safe: you can add the abstraction later.
- Many refactorings end at a design pattern — that's the healthy way to arrive at one.
Key terms
- Refactoring — Restructuring code without changing its observable behaviour.
- Characterisation test — A test that records what existing code does now, so changes can be checked against it.
- Preparatory refactoring — Reshaping code first so the upcoming change becomes simple.
- Rewrite — Replacing code wholesale; riskier and not a refactoring.