What Is Refactoring?
A precise definition
Section titled “A precise definition”People use refactoring loosely to mean “cleaning up code.” The discipline means something sharper:
A refactoring is a change to the code that improves its internal structure while preserving its observable behavior.
Two halves matter equally. The change must improve structure — otherwise it is pointless churn. And it must preserve behavior — every input still produces the same output, every side effect still happens, no public contract shifts. A change that alters what callers can observe is not a refactoring, no matter how tidy the result looks.
Refactoring vs. rewriting vs. adding features
Section titled “Refactoring vs. rewriting vs. adding features”These three activities feel similar from a chair, but they carry very different risks.
- Refactoring reshapes existing, working code in small reversible steps. You always have a running program; you are never far from the last green state.
- Rewriting throws the old code away and builds a replacement. You lose the accumulated bug fixes baked into the original, and you have no running program until the new one is done. Sometimes necessary, rarely cheap.
- Adding a feature introduces new behavior. The program does something it could not do before, which means its observable behavior changes by design.
Refactoring is the safe middle path: you keep the working code and improve it gradually, rather than gambling on a from-scratch replacement.
The two hats
Section titled “The two hats”When you sit down to edit code, you are wearing one of two hats. The feature hat is for adding capability: you write new code and the tests for it. The refactor hat is for restructuring: you change how the code is shaped, and the existing tests must stay green the whole time because behavior must not move.
The rule is simple and strict: wear only one hat at a time. If you are halfway through extracting a function and notice a missing feature, finish the refactor, commit, then switch hats. Mixing the two is how a “quick cleanup” turns into an afternoon of debugging — when a test breaks, you can no longer tell whether your restructuring or your new feature caused it.
flowchart TB
Start["I want to work on the code"] --> Q{"Which hat?"}
Q -->|"Adding behavior"| Feature["Feature hat:<br/>write new code,<br/>add tests"]
Q -->|"Cleaning structure"| Refactor["Refactor hat:<br/>keep behavior,<br/>tests stay green"]
Feature --> Switch["Switch hats —<br/>never wear both"]
Refactor --> Switch A tiny before → after
Section titled “A tiny before → after”The smallest refactorings are often the most valuable. Here we do two: replace a magic number with a named constant, and rename a cryptic variable. Behavior is identical before and after — the output is the same — but the intent now reads off the page.
// Beforefunction price(q: number): number { return q * 9.99 * 1.07;}
// Afterconst UNIT_PRICE = 9.99;const TAX_RATE = 1.07;
function price(quantity: number): number { return quantity * UNIT_PRICE * TAX_RATE;}# Beforedef price(q): return q * 9.99 * 1.07
# AfterUNIT_PRICE = 9.99TAX_RATE = 1.07
def price(quantity): return quantity * UNIT_PRICE * TAX_RATE// Beforefunc Price(q float64) float64 { return q * 9.99 * 1.07}
// Afterconst ( UnitPrice = 9.99 TaxRate = 1.07)
func Price(quantity float64) float64 { return quantity * UnitPrice * TaxRate}// Beforefn price(q: f64) -> f64 { q * 9.99 * 1.07}
// Afterconst UNIT_PRICE: f64 = 9.99;const TAX_RATE: f64 = 1.07;
fn price(quantity: f64) -> f64 { quantity * UNIT_PRICE * TAX_RATE}Nothing the function returns has changed. That is the whole point: a refactoring you can run before and after, and get byte-for-byte identical results, is a refactoring you can trust.