Refactoring Techniques
Learning Objectives
Apply core refactorings: extract method/class/interface, move member, encapsulate field, replace conditional with polymorphism, introduce parameter/value object, separate query from modifier, break cycles.
Refactor safely with tests and small steps.
Prerequisites: Module 52.
Intuition and Definition
Refactoring is changing the internal structure of code without changing its observable behaviour, in small, verifiable steps (Fowler). The safety net is a test suite that pins the current behaviour.
long block with a comment class with two clusters of fields/methods client depends on a concrete class client depends on an interface method moved to the data's owner private field + intent-revealing operations Replace conditional with polymorphism switch(type) repeated in many places virtual method per type (only when justified) Replace inheritance with composition delegate to a member Introduce parameter object (start, end, tz) everywhere Replace primitive with value object std::string vin Separate query from modifier int popAndCount() pop() + count() Introduce dependency injection new Db() inside interface owned by A, implemented by B
Why It Matters
Design is never right the first time. Refactoring lets the design evolve with the requirements without rewrites.
Syntax and Language Notes
IDEs perform many refactorings mechanically and safely (rename, extract, move, change signature). Prefer them over hand edits — they update every reference.
Complete Example
"Replace conditional with polymorphism", step by step. The characterization test (same inputs, same outputs) runs on both versions:
#include <cassert>
#include <iostream>
#include <memory>
#include <string>
#include <vector>
// ---- before: type code + switch, repeated wherever behaviour differs ----
struct VehicleRec { std::string kind; double km; int axles; };
double tollBefore(const VehicleRec& v) {
if (v.kind == "car") return 0.05 * v.km;
if (v.kind == "truck") return 0.12 * v.km * v.axles / 2.0;
if (v.kind == "bus") return 0.03 * v.km;
return 0;
}
// ---- after: each kind owns its rule; adding a kind no longer edits a switch ----
struct Vehicle { virtual ~Vehicle() = default; virtual double toll(double km) const = 0; };
struct Car : Vehicle { double toll(double km) const override { return 0.05 * km; } };
struct Truck : Vehicle {
int axles; explicit Truck(int a) : axles(a) {}
double toll(double km) const override { return 0.12 * km * axles / 2.0; }
};
struct Bus : Vehicle { double toll(double km) const override { return 0.03 * km; } };
std::unique_ptr<Vehicle> fromRecord(const VehicleRec& r) { // the ONE remaining switch, at the boundary
if (r.kind == "car") return std::make_unique<Car>();
if (r.kind == "truck") return std::make_unique<Truck>(r.axles);
if (r.kind ==
…car 100 km: 5
truck 100 km: 24
bus 250 km: 7.5
truck 37.5 km: 6.75
all 4 characterization checks passedStep-by-Step Walkthrough
Write the characterization test against the old function first.
Introduce the Vehicle interface and one class per kind, copying each branch's logic verbatim.
Replace callers to use fromRecord(...)->toll(...); the switch now exists once, at the data boundary.
Run the test after every step — identical outputs prove behaviour was preserved.
Only then improve the rules themselves (separate commit).
Common Mistakes
Refactoring and changing behaviour in the same commit — impossible to review or bisect.
No tests before refactoring legacy code.
Replacing every conditional with polymorphism — a single, local switch is often clearest.
Big-bang rewrites instead of incremental strangling.
Trade-offs and Alternatives
Polymorphism helps when the same type switch appears in many places or types grow; a std::variant + std::visit keeps exhaustiveness checking for closed sets; a lookup table suits data-like differences (rates per kind).
Real-World Use
Daily work in any long-lived codebase; often guided by static analysis and code review. The "strangler fig" approach replaces legacy subsystems gradually behind interfaces.
Practice
Q1. (Easy) What must stay unchanged during a refactoring?
Answer: Observable behaviour.
Q2. (Medium) What is a characterization test?
Answer: A test that captures what the code currently does (not what it should do), used as a safety net before refactoring.
Q3. (Advanced) In which order would you refactor a method with feature envy, a long body and a boolean flag?
Answer: Add tests; split the boolean flag into two explicit entry points (callers become readable); extract methods for the steps; then move the envious extracted method to the class whose data it uses. Each step separately, tests green after each.
Interview Question
Q1. "When would you *not* replace a switch statement with polymorphism?"
Answer: When the switch occurs in one place, the set of cases is small and stable, the cases are data-like (different constants rather than different behaviour — use a table), or the types are a closed set where std::variant/pattern matching gives exhaustiveness checks. Polymorphism pays off when the same switch is duplicated across the codebase or new types are added regularly.
Quick Quiz
Q1. Who coined the modern definition of refactoring?
Answer: Martin Fowler (*Refactoring*, 1999/2018).
Q2. Safest tool for renaming across a codebase?
Answer: The IDE's automated rename refactoring.
Q3. Refactoring for "the same three parameters everywhere"?
Answer: Introduce parameter object.
Summary
Refactoring = structure changes, same behaviour, small steps, tests first.
Use automated tools; separate refactoring commits from behaviour changes.
Related: Modules 52, 54, 64.