LESSON 10.4
Refactoring toward a pattern step by step
Safely evolve tangled legacy procedural code toward clean design patterns using automated test safety nets and incremental refactoring steps.
6 min read
Legacy systems frequently contain massive conditional statements that have grown over years. Attempting a wholesale "big bang" rewrite is suicidal: subtle edge cases are lost, deadlines are missed, and regressions flood production.
The safe path is Refactoring toward a Pattern: incrementally transforming code step by step while keeping the automated test suite green at every checkpoint.
The 5-Step Refactoring Process
- Build the Safety Net: Write automated unit tests pinning down current observable behavior (including quirky edge cases).
- Define the Target Abstraction: Create the new interface or abstract class (e.g.
FeeCalculationStrategy). - Extract the First Branch: Move one branch into a concrete implementation class.
- Delegate and Verify: Call the new class from within the existing switch statement. Run tests.
- Eliminate the Switch: Once all branches are extracted, replace the switch statement with dynamic strategy dispatch (or a registry map).
In code
// STEP 1: Legacy code with brittle switch statement
class LegacyParkingLot {
calculateFee(vehicleType, hours) {
// Brittle conditional growing with every vehicle type
if (vehicleType === "CAR") {
return hours * 5.0;
} else if (vehicleType === "BIKE") {
return hours * 2.0;
} else if (vehicleType === "BUS") {
return Math.max(20.0, hours * 10.0);
}
throw new Error(`Unknown vehicle: ${vehicleType}`);
}
}
// STEP 2 & 3: Define Strategy interface and extract concrete classes
class ParkingStrategy {
compute(hours) { throw new Error(); }
}
class CarParkingStrategy extends ParkingStrategy {
compute(hours) { return hours * 5.0; }
}
class BikeParkingStrategy extends ParkingStrategy {
compute(hours) { return hours * 2.0; }
}
class BusParkingStrategy extends ParkingStrategy {
compute(hours) { return Math.max(20.0, hours * 10.0); }
}
// STEP 4 & 5: Clean Refactored Context using Strategy Map
class RefactoredParkingLot {
#strategies = new Map([
["CAR", new CarParkingStrategy()],
["BIKE", new BikeParkingStrategy()],
["BUS", new BusParkingStrategy()]
]);
calculateFee(vehicleType, hours) {
const strategy = this.#strategies.get(vehicleType);
if (!strategy) throw new Error(`Unknown vehicle: ${vehicleType}`);
return strategy.compute(hours);
}
// Open/Closed: new vehicle types registered dynamically!
registerVehicle(type, strategy) {
this.#strategies.set(type, strategy);
}
}
// Verification: both implementations produce identical outputs
const legacy = new LegacyParkingLot();
const refactored = new RefactoredParkingLot();
console.assert(legacy.calculateFee("CAR", 3) === refactored.calculateFee("CAR", 3));
console.assert(legacy.calculateFee("BUS", 1) === refactored.calculateFee("BUS", 1));
// New requirement added with ZERO changes to existing classes:
refactored.registerVehicle("EV", { compute: (h) => h * 3.0 });
console.assert(refactored.calculateFee("EV", 4) === 12.0);
console.log("Refactoring verified. EV Fee:", refactored.calculateFee("EV", 4));public class Example {
interface ParkingStrategy {
double compute(int hours);
}
static final class CarStrategy implements ParkingStrategy {
public double compute(int hours) { return hours * 5.0; }
}
static final class
When to use it, and when not to
Use incremental refactoring when:
- You have high-risk legacy code that is poorly documented but financially critical.
- You want to modernize code without freezing feature delivery for months.
Don't use it when:
- The legacy code is completely untestable throwaway prototype code: a fresh rewrite is sometimes cleaner if requirements are tiny and well-isolated.
Where you have already seen it
Martin Fowler's classic book Refactoring: Improving the Design of Existing Code and Joshua Kerievsky's Refactoring to Patterns catalog this exact evolutionary path. Modern IDEs (IntelliJ, VS Code) provide automated refactoring tools (Extract Method, Extract Interface) to execute these transformations safely.
Related patterns
- Refactoring to Strategy: Replacing
switch(type)with Strategy objects. - Refactoring to State: Replacing
switch(state)and flag variables with State transitions. - Refactoring to Decorator: Replacing combinatorial inheritance with composable wrappers.
Try it
Parking Lot LLD
Take a procedural parking lot fee calculation and refactor it into clean Strategy and Vehicle polymorphic classes.