Refactoring#

Refactoring is changing the structure of code without changing its behavior. It is a discipline, not a project: small, reversible transformations applied continuously, each one safe in isolation. The goal is a codebase that stays cheap to change as it grows.

The standard reference is Martin Fowler’s Refactoring: Improving the Design of Existing Code (2nd ed., 2018). The catalog below is a working subset; consult Fowler for the full taxonomy and step-by-step mechanics.

When to Refactor#

Refactoring is most useful as a habit, not an event. The four healthy modes below cover the legitimate reasons to reshape code; the section that follows lists the situations where refactoring is the wrong move.

  • Preparatory, before adding a feature, reshape the code so the feature is easy to add. Kent Beck: “make the change easy, then make the easy change.”

  • Comprehension, as you read code to understand it, refine names and extract helpers so the next reader pays a smaller tax.

  • Litter-pickup, the boy-scout rule: leave the file cleaner than you found it. Small, opportunistic.

  • Planned, a tracked piece of work to pay down a specific debt (e.g., remove a deprecated module). Rare; should be justified by an upcoming change that needs it.

When not to refactor.

  • Code that is correct, stable, and unlikely to be touched again.

  • When you do not have tests or another safety net for the area.

  • In the same commit as a behavior change, separate the two.

  • When the deadline is real and the rewrite is speculative.

Code Smells#

Smells are heuristics for where a refactoring may help. They are indicators, not diagnoses; a long function may be the right form for a particular problem, but it earns more scrutiny when you see one. The catalog below pairs each smell with the refactoring it usually wants.

Smell

What it suggests

Long Function

Extract Function; one function should do one thing at one level.

Large Class

Extract Class; the class is tracking more than one responsibility.

Long Parameter List

Introduce Parameter Object; or the function is doing too much.

Duplicated Code

Extract Function / Pull Up Method; rule of three.

Divergent Change

One module changes for many unrelated reasons -> Split Module.

Shotgun Surgery

One change touches many modules -> Move related code together.

Feature Envy

A method uses another object’s data more than its own -> Move Method.

Data Clumps

The same fields travel together -> Extract Class / Parameter Object.

Primitive Obsession

Strings/ints used where a domain type belongs -> Replace Primitive with Object.

Switch Statements

Repeated dispatch on a type code -> Replace Conditional with Polymorphism.

Temporary Field

Field only valid sometimes -> Extract Class for the lifecycle.

Message Chains

a.getB().getC().getD() -> Hide Delegate.

Middle Man

A class that only delegates -> Remove Middle Man.

Speculative Generality

Hooks for needs that never materialized -> Inline / Collapse Hierarchy.

Comments

Comments compensating for unclear code -> Extract / Rename until the comment is redundant.

Reference: Fowler, Refactoring (2nd ed.), ch. 3.

Core Refactorings#

The transformations you will reach for most often. Each is mechanical and reversible, which is what makes refactoring safe. The catalog below is a working subset of Fowler’s full taxonomy; the most-used ones (Extract Function, Rename) are at the top.

Refactoring

Effect

Extract Function

Pull a fragment into a named function. The single most common refactoring.

Inline Function

The body is clearer than the call, inline it.

Extract Variable

Name an expression to expose intent. Reverse: Inline Variable.

Rename

Better names. The most underrated refactoring.

Change Function Declaration

Add, remove, reorder, or rename parameters.

Encapsulate Variable

Wrap shared data behind functions before changing it.

Move Function / Move Field

Relocate to the module or class that uses it most.

Slide Statements

Group related statements together; precondition for Extract Function.

Split Phase

Separate a function that does two things in sequence (e.g., parse then evaluate).

Replace Loop with Pipeline

Loops with intermediate state become map/filter/reduce.

Replace Conditional with Polymorphism

switch on type code -> subclasses or strategy objects.

Replace Magic Literal

Named constant for a literal that carries meaning.

Introduce Parameter Object

Group co-traveling parameters into a value object.

Combine Functions into Class

Functions that share data and gravitate toward each other.

Decompose Conditional

Extract the condition and each branch into named functions.

Replace Nested Conditional with Guard Clauses

Early returns flatten deeply nested if trees.

Hide Delegate

Client talks to a, not a.getB(); reduces coupling.

Remove Dead Code

Trust version control. Delete it.

Reference: Fowler, Refactoring (2nd ed.), chs. 6-12.

The Mechanics#

A refactoring is safe only if you do it the same way every time. Skipping the discipline is what turns “small refactor” into “the change that broke everything.” The generic loop.

  1. Tests green before starting. If there are none, write

    characterization tests first, tests that pin down current behavior, even if that behavior is wrong.

  2. One transformation at a time. Atomic commits. Each commit

    compiles and the tests pass.

  3. Small steps. If a step is large enough that you can’t keep the

    whole thing in your head, break it down.

  4. Run the tests after each step. Not at the end.

  5. Commit when green. Easy revert if the next step goes sideways.

  6. Separate refactoring commits from behavior-change commits. A PR

    can contain both, but never in the same commit.

Example: extracting a helper while keeping each commit green.

Example.

$ git status
$ pytest -q

$ $EDITOR billing.py
$ pytest -q && git commit -am "refactor: extract _apply_discount helper"

$ $EDITOR billing.py
$ pytest -q && git commit -am "refactor: use _apply_discount in checkout"

$ $EDITOR billing.py
$ pytest -q && git commit -am "refactor: drop inlined discount block"

Tests as the Safety Net#

Refactoring without tests is rewriting with hope. The test suite is what makes “behavior unchanged” a verifiable claim, not an aspiration; the speed of the suite determines how often the discipline is worth the friction. The principles below are how good test suites support good refactoring.

  • Fast feedback, if the suite takes minutes, you will refactor less. Aim for seconds in the inner loop (see Testing).

  • Test behavior, not structure, tests coupled to private internals make refactoring harder. Test through public seams.

  • Characterization tests for legacy code, write tests that lock in what the code currently does, then refactor against them.

  • Coverage as a floor, not a ceiling, coverage tells you what ran, not whether the assertions are meaningful.

Reference: Michael Feathers, Working Effectively with Legacy Code.

Large-Scale Refactoring#

Some refactorings are too big for one commit or one PR. Treat them as migrations, multi-week, on main, with parallel implementations and a deliberate cutover. The patterns below are the standard tactics; the standard anti-pattern is the “big rewrite” that stops the world.

  • Branch by abstraction, introduce a seam, route both implementations through it, switch readers, retire the old path.

  • Strangler Fig, new code wraps old; functionality migrates piece by piece until the old system can be deleted. Coined by Martin Fowler.

  • Expand / contract (parallel change), add the new structure, dual-write or dual-read, migrate consumers, remove the old structure.

  • Feature flag the cutover, so rollback is a config change, not a revert.

  • Incremental on main, avoid long-lived refactor branches; they diverge and rot.

Anti-pattern: the rewrite. Stopping the world to rewrite a system from scratch almost always costs more, takes longer, and ships less than incremental refactoring of the existing one. Joel Spolsky, “Things You Should Never Do, Part I” (2000) is still the standard warning.

Tooling#

Modern editors and IDEs make many refactorings near-zero-cost; using them is the difference between “this rename is two hours of grep” and “this rename is one keystroke.” The table below maps each tool category to what it actually does for the operator.

Tool

What it does

IDE refactor menu

Rename, Extract Function, Move, Inline, type-aware and safe. Available in JetBrains IDEs, VS Code (via language servers), Visual Studio, Eclipse.

Language servers

Power refactorings in editors that don’t ship them natively (rust-analyzer, gopls, pyright, clangd).

ast-grep / comby

Structural search-and-replace across a codebase. Refactorings expressed as patterns over the AST, not regex.

codemod / jscodeshift

Programmatic, repeatable refactorings for JavaScript / TypeScript.

rope / libcst / Bowler

Python refactoring libraries usable from scripts.

gofmt / ruff / rustfmt / prettier

Formatters and linters that absorb the bikeshedding so reviews can focus on substance.

Version control

The ultimate safety net. git bisect finds the commit that changed behavior; git reflog recovers from a bad rebase.

Example: a structural rename with ast-grep and a JS codemod with jscodeshift.

Example.

$ ast-grep --pattern '_apply_discount($A)' \
         --rewrite  'apply_discount($A)' \
         --lang python -U

$ npx jscodeshift -t ./codemods/rename-getUser.ts \
                --extensions=ts,tsx --parser=tsx src/

Refactoring in Code Review#

The etiquette around mixing refactor and feature work in review. The rule of thumb: separate when the refactor is its own readable unit, mixed when it’s small and obviously preparatory, never drive-by inside someone else’s PR.

  • Separate refactoring PRs from feature PRs when the refactor is large enough to read as its own unit. Reviewers can verify “no behavior change” by reading the diff plus the test results.

  • Mixed PRs are fine when the refactor is small and obviously preparatory (“extract this helper so the next commit can use it”). Mark the refactor commits in the PR body so reviewers know which to skim.

  • No drive-by rewrites in someone else’s PR, propose a follow-up.

  • Resist the urge to refactor unrelated code while reviewing. Open a ticket; do it in its own PR.

See Practices for code review practices generally.

What Refactoring Is Not#

The four boundary clarifications worth making explicit. Refactoring isn’t rewriting; isn’t aesthetic cleanup; isn’t optional; isn’t free. Each one pushes back against a common misuse of the word that ends up costing teams.

  • Not rewriting, a rewrite changes both structure and behavior, often much of both. Refactoring is behavior-preserving by definition.

  • Not “cleanup”, “cleanup” implies aesthetic taste; refactoring is a set of named, mechanical transformations.

  • Not optional, code that is never refactored ossifies. Each new feature pays a higher tax than the last, until the team can no longer ship at the rate the business needs.

  • Not free, it costs reviewer time and risks regressions. The point of small steps and tests is to keep that cost predictable.

References#