Code Refactoring in Practice: From "It Works" to "Easy to Change"
Refactoring is not about "rewriting bad code" — it is about improving code structure step by step, with verification at each step. This article covers when to refactor, how to refactor safely, common refactoring techniques, and how to make refactoring a daily habit — for backend and frontend developers maintaining medium-to-large projects.
The Bottom Line: Refactoring Is Not “Rewriting” — It Is “Improving”
Many developers react to bad code with “let’s rewrite it,” but rewriting carries much higher risk than refactoring — you may lose edge case handling, introduce new bugs, and business stakeholders will not wait for you to “finish the rewrite.”
The core principle of refactoring: change one thing at a time, and make sure it still works after each change.
1. When to Refactor
Signals Worth Acting On
| Signal | Symptom | Solution |
|---|---|---|
| Duplicated code | Same logic in multiple places | Extract function or class |
| Long function | Over 50 lines | Split into smaller functions |
| Large class | Over 500 lines | Split responsibilities |
| Tight coupling | Changing one thing breaks many | Introduce interfaces or DI |
| Poor naming | Variable names do not express intent | Rename |
When NOT to Refactor
- Code is about to be replaced (scheduled for decommission)
- Code is rarely modified (stable, untouched)
- No test coverage and cannot be added (legacy system nearing end of life)
2. Safe Refactoring Steps
2.1 Write Tests First
describe('formatPrice', () => {
it('formats price correctly', () => {
expect(formatPrice(1000)).toBe('¥1,000.00');
expect(formatPrice(2500.5)).toBe('¥2,500.50');
});
});
2.2 Small Steps
One refactoring operation at a time, run tests after each:
// Step 1: Rename
function calc(a: number, b: number) { ... }
// → function calculateDiscount(originalPrice: number, level: string) { ... }
// Step 2: Extract function
const total = items.reduce((sum, i) => sum + i.price * i.qty, 0);
const tax = total * 0.1;
// → function calculateSubtotal(items: Item[]): number { ... }
// → function calculateTax(subtotal: number): number { ... }
2.3 Commit Each Step
git commit -m "refactor: extract calculateDiscount function, replace inline calculation"
3. Common Refactoring Techniques
| Technique | Best For | Note |
|---|---|---|
| Rename | Poor naming | IDE auto-refactoring, safe |
| Extract Function | Long functions, duplication | Extract logic into independent function |
| Extract Variable | Hard-to-understand expressions | Use meaningful variable names |
| Decompose Condition | Long conditionals | Split into multiple functions |
| Introduce Parameter | Hard-coded values | Replace with parameter |
| Move Method | Method in wrong class | Move to more appropriate class |
4. Make Refactoring a Habit
The Boy Scout Rule
“Leave the camp cleaner than you found it” — every time you modify code, improve it slightly. No need to schedule dedicated refactoring time.
Time Budget
| Scenario | Suggested Time |
|---|---|
| Bug fix | 5-10 min of refactoring |
| Small feature | 15-30 min refactoring affected code first |
| Large feature | Plan 1-2 days for refactoring |
| Code Review | Note smells for next refactoring session |
Summary
| Principle | Note |
|---|---|
| Test first | Refactoring without tests is gambling |
| Small steps | One refactoring operation at a time |
| Frequent commits | One commit per step, easy to rollback |
| Daily habit | No “refactoring month” |
| Boy Scout rule | Improve a little every time |
Refactoring is not about “waiting until the code is terrible and then doing a big overhaul” — it is about “making it a little better every time you pass through.” A codebase that improves 1% every day, versus one that accumulates 1% technical debt every day, will be vastly different in six months.
Need code refactoring or technical consulting? Contact us — tell us about your project scale and pain points, feasibility within 24 hours.
FAQ
What is the difference between refactoring and rewriting?
Refactoring improves internal structure without changing external behavior — functionality stays the same, code gets better. Rewriting means starting from scratch — functionality may change, code is completely rewritten. Every refactoring step is verifiable (tests pass), with controllable risk. Rewriting carries much higher risk, especially without test coverage. Recommendation: prefer refactoring over rewriting unless the code is completely unmaintainable.
When should you refactor?
Three scenarios: ① Before adding a new feature — refactor the affected code first to make the new feature easier to add (the Boy Scout Rule: leave the camp cleaner than you found it); ② When fixing a bug — if the bug was caused by hard-to-understand code, refactor first, then fix; ③ When you spot code smells — duplicated code, long functions, large classes, excessive coupling. Do not schedule a "refactoring month" — refactoring should be part of daily development.
What if there are no tests?
Write tests first, then refactor. Refactoring without tests is gambling. Approach: ① Write characterization tests to lock in current behavior — assert the current output regardless of whether it is correct; ② Refactor, ensuring the output stays the same; ③ If you find a bug, fix it first, then refactor.
How do you break down a large refactoring?
Large refactorings must be broken into small steps. Each step must leave the code runnable and testable. Recommended approaches: ① By module — refactor one module at a time, verify, then continue; ② By layer — refactor the test layer first, then the interface layer, then the implementation layer; ③ Use the Strangler Fig pattern — write new code for new features, gradually migrate callers, then delete the old code.
This article comes from AI Enable Harness front-line delivery practice. Need a similar system or optimization service?
Subscribe to Updates
Get notified when new articles are published. No spam, occasional updates only.
Subscribe →