Split Phase — Refactoring Patterns, Part 8
Refactoring Patterns · Part 8
Some functions do two unrelated jobs tangled together — parse the input and compute the result, or fetch the data and format it — which makes both jobs harder to change and nearly impossible to test in isolation. Split Phase pulls them apart into stages connected by a clean intermediate. This walks through the refactor, and why the testing win alone usually pays for it.
One function doing two things is two functions trying. Split them.
Step 1: The Smell (15 min)
function processOrder(rawOrder: string) {
// parse
const [id, items, total] = rawOrder.split(',')
// calculate
const tax = parseFloat(total) * 0.08
const shipping = items.length > 5 ? 0 : 10
// persist
db.save({ id, items, total, tax, shipping })
}
Three phases mashed together. Hard to test each.
Step 2: After (15 min)
function parseOrder(rawOrder: string): Order {
const [id, items, total] = rawOrder.split(',')
return new Order(id, items, parseFloat(total))
}
function applyTotals(order: Order): OrderWithTotals {
const tax = order.total * 0.08
const shipping = order.items.length > 5 ? 0 : 10
return { ...order, tax, shipping }
}
function processOrder(rawOrder: string) {
const order = parseOrder(rawOrder)
const withTotals = applyTotals(order)
db.save(withTotals)
}
Each phase: testable separately. Composable.
Step 3: The Two Phases (15 min)
Classic split:
Phase 1: parse / normalize / validate input
Phase 2: business logic on clean data
Each phase: simpler than mixed version.
Step 4: When to Apply (15 min)
Function does heterogeneous things
Hard to test pieces in isolation
Mixing I/O with logic
Mixing parsing with business rules
If you'd describe function with "and": split.
Step 5: Intermediate Data Structure (15 min)
The output of phase 1 becomes input to phase 2:
interface ParsedOrder {
id: string
items: Item[]
total: number
}
Clean type. Phase 2 doesn't worry about input format.
Step 6: I/O at Edges (15 min)
Common pattern:
Phase 1 (top): I/O input
Phase 2 (middle): pure logic
Phase 3 (bottom): I/O output
Functional core, imperative shell.
Step 7: When NOT (15 min)
Trivial functions
One-off scripts
Performance-critical inner loops (allocation cost)
Most app code: benefits from split. Hot loops: profile first.
Step 8: Steps (15 min)
Identify phases by adding comments / blank lines
Extract phase 2 into function (Part 2: Extract Function)
Define intermediate data structure
Run tests
Commit
Extract phase 1 if needed
Run tests
Commit
Small steps.
Step 9: Pipeline Style (15 min)
const result = pipe(
rawInput,
parse,
validate,
applyDiscount,
applyTax,
save
)
Many split phases → pipeline. Each step's input is previous's output.
Step 10: Testing Wins (15 min)
test('parseOrder handles missing fields', () => { ... })
test('applyTotals applies 8% tax', () => { ... })
test('processOrder full flow', () => { ... })
Each phase has focused tests. Failure isolation easier.
What You Just Did
Split Phase: the smell, after, the two phases, when to apply, intermediate data structure, I/O at edges, when not, steps, pipeline style, testing wins.
Common Failure Modes
Over-split tiny functions. Noise.
Phase 2 still does I/O. Defeats purpose.
No intermediate type. Coupling persists.
Mix in retry / error handling badly. Cross-cutting smear.
Stop after first split. Missed deeper structure.
Continue the Refactoring Patterns path
Previous — Part 7: Introduce Parameter Object
Next — Part 9: Refactor to Tests
Part of the Refactoring Patterns learning path.


