Refactor
“Any fool can write code that a computer can understand. Good programmers write code that humans can understand.” — Martin Fowler
Understand This First
- Test – tests make refactoring safe.
Context
Your code works. The tests pass. But the internal structure is messy: duplicated logic, unclear names, tangled responsibilities. You need to improve the design without breaking what already works. This is a tactical pattern that operates on the internal quality of code while preserving its external behavior.
Refactoring depends on having Tests that verify the code’s behavior. Without tests, you’re not refactoring; you’re just editing and hoping.
Problem
Code accumulates mess over time. Quick fixes, changing requirements, and the natural pressure to ship all contribute to structural decay. Code that was clear last month becomes confusing this month. Duplicated logic appears in three places. A function that started simple now handles five different cases. The code still works, for now, but every change takes longer and is more likely to introduce bugs. How do you clean up without breaking things?
Forces
- Working code is valuable; breaking it to “improve” it destroys value.
- Messy code slows down every future change.
- Cleaning up feels unproductive because no new features are added.
- Without tests, it’s hard to know whether a structural change preserved behavior.
- Some improvements require touching many files, increasing risk.
Solution
Change the internal structure of the code without changing its external behavior. Refactoring isn’t adding features, fixing bugs, or optimizing performance; it’s reorganizing what you already have so that it’s clearer, simpler, and easier to change.
Common refactoring moves include:
- Rename — giving a variable, function, or class a clearer name.
- Extract — pulling a block of code into its own function with a descriptive name.
- Inline — replacing a function call with its body when the indirection adds no clarity.
- Move — relocating code to the module or class where it logically belongs.
- Simplify conditionals — untangling nested
ifstatements into clearer structures.
The critical discipline is to make one small change at a time and run the tests after each change. If a test fails, you undo the last change and try a smaller step. This is refactoring, not rewriting. A rewrite throws away the old code and starts fresh; a refactoring transforms it incrementally, preserving behavior at every step.
How It Plays Out
A checkout module has grown to 500 lines. Tax calculation, discount logic, and payment processing are all tangled together. A developer extracts the tax calculation into its own function, runs the tests (all green). Then they extract the discount logic (all green). Then they move the payment processing into a separate module (all green). The checkout module is now 150 lines, and each piece can be understood and changed independently.
In agentic coding, refactoring is one of the safest tasks to delegate to an AI agent. You point the agent at a function and say “extract the validation logic into a separate function” or “rename these variables for clarity.” Because refactoring doesn’t change behavior, the existing tests verify the agent’s work. If the tests still pass, the refactoring is correct by definition. This makes refactoring an ideal early task for building trust with an agent.
When asking an agent to refactor, be specific about the transformation: “extract,” “rename,” “split this function.” Vague instructions like “clean this up” may produce surprising changes that are hard to review.
“Extract the tax calculation logic from the checkout function into its own function called calculate_tax. Don’t change any behavior — the existing tests should all pass without modification.”
Consequences
Regular refactoring keeps code maintainable. It reduces the cost of future changes, makes bugs easier to find, and makes the codebase more welcoming to new developers and AI agents. Code that’s regularly refactored accumulates less technical debt.
The cost is time spent not shipping features. Refactoring requires discipline: the willingness to improve code that already works. It also requires Tests. Refactoring without tests is like performing surgery without anesthesia: possible, but nobody enjoys the outcome. If your test coverage is thin, invest in tests before refactoring.
Related Patterns
- Depends on: Test — tests make refactoring safe.
- Enabled by: Red/Green TDD — the refactoring phase is built into every TDD cycle.
- Preserves: Invariant — refactoring must not violate established invariants.
- Prevents: Regression — disciplined refactoring with tests avoids introducing regressions.
- Enabled by: Test-Driven Development — TDD creates the safety net that makes refactoring safe.
- Related: Big Ball of Mud – refactoring is the primary tool for reclaiming structure from mud.
- Related: Premature Optimization – optimized code resists refactoring.
- Related: Technical Debt – refactoring is the primary mechanism for paying down debt.
Sources
- William Opdyke formalized refactoring as a disciplined technique in his 1992 PhD thesis Refactoring Object-Oriented Frameworks at the University of Illinois, supervised by Ralph Johnson. Opdyke and Johnson coined the term and defined the first catalog of behavior-preserving code transformations.
- Martin Fowler’s Refactoring: Improving the Design of Existing Code (1999, 2nd ed. 2018) popularized the practice and established the vocabulary of named refactoring moves — Extract, Rename, Inline, Move — that this article draws on. The epigraph quote is from this work.
- Kent Beck connected refactoring to testing through Extreme Programming and the red-green-refactor cycle in Test-Driven Development: By Example (2003), making refactoring a routine part of development rather than an occasional cleanup activity.
- Ward Cunningham coined the “technical debt” metaphor at OOPSLA 1992, describing how deferred code cleanup accumulates interest — the framing this article uses in its Consequences section.