Refactoring Patterns
Apply proven refactoring moves — extract, inline, move, introduce parameter — that are behavior-preserving and testable.
Why
Refactoring is changing structure without changing behavior, and the only way to do that safely is in small, behavior-preserving steps that a test suite can verify after each move. Big-bang rewrites change behavior and structure simultaneously, so when tests fail you can't tell which change broke them. The catalog of named refactoring patterns (Fowler's) gives you a vocabulary of moves that are proven behavior-preserving: extract function, inline variable, move method, introduce parameter object. Each is small enough to commit separately, each is verifiable by tests, and composing them achieves large structural changes through a chain of safe small ones. This is how you pay design debt without a rewrite.
When
When paying identified design debt, or when a feature is hard to add because the structure fights you. Never during a bug fix (fix first, refactor after).
How
1. Ensure tests exist and are green before starting — no tests = no refactor, write characterization tests first 2. Pick one pattern from Fowler's catalog that moves toward the target structure 3. Apply the move in the smallest possible step; commit "refactor: extract function X" 4. Run tests; if red, the move wasn't behavior-preserving — revert and reconsider 5. Repeat with the next pattern until the target structure is reached 6. Keep each commit <50 lines diff so review is trivial and bisect is meaningful 7. Never mix refactor commits with feature/bugfix commits — a mixed commit can't be reverted safely
Example
# Before: a function doing 3 things (hard to test, hard to extend)
function processOrder(order) {
// validate
if (!order.items || order.items.length === 0) throw new Error("empty");
// calculate
let total = 0;
for (const item of order.items) total += item.price * item.qty;
// save
db.orders.insert({ ...order, total });
return total;
}
# Step 1: extract validateOrder (commit: "refactor: extract validateOrder")
function validateOrder(order) {
if (!order.items || order.items.length === 0) throw new Error("empty");
}
function processOrder(order) {
validateOrder(order); // ... rest unchanged
}
# tests green? commit.
# Step 2: extract calculateTotal (commit: "refactor: extract calculateTotal")
# Step 3: extract saveOrder (commit: "refactor: extract saveOrder")
# Now processOrder is 3 lines that compose 3 named, testable functions.Common Problems
- ⚠Refactoring without tests — you're now changing structure AND behavior and can't tell, which is a rewrite, not a refactor.
- ⚠Big-bang refactors in one commit — when tests break you can't isolate which of 20 moves caused it, so you revert the whole thing.
- ⚠Mixing refactor and feature work in one PR — the reviewer can't tell which changes are behavior and which are structure.
Best Practices
- ✓Never refactor without green tests — if tests don't exist, write characterization tests first that pin current behavior.
- ✓Commit one refactoring move at a time with a "refactor:" prefix — each commit is behavior-preserving and independently revertible.
- ✓Keep refactors out of feature PRs — a pure refactor PR that's green is trivial to review and merge; a mixed one is not.