The Art of Code Refactoring: When and How to Refactor Legacy Code
Refactoring is the process of improving code without changing its behavior. It's one of the most important skills for maintaining a healthy codebase. After refactoring thousands of lines of legacy code, I've learned that refactoring is both an art and a science.
What is Refactoring?
Definition
Refactoring is changing the structure of code without changing its functionality.
Goals
- Improve readability - Easier to understand
- Reduce complexity - Simpler code
- Improve maintainability - Easier to change
- Remove duplication - DRY principle
- Improve design - Better architecture
When to Refactor
Good Times to Refactor
- Before adding features - Clean code makes adding features easier
- When fixing bugs - Clean up the area while you're there
- During code review - Improve code before it merges
- When understanding code - Refactor to understand better
- Regular maintenance - Scheduled refactoring time
Bad Times to Refactor
- During critical deadlines - Focus on delivery
- When code works fine - Don't fix what isn't broken
- Without tests - Too risky
- Large refactors - Break into smaller pieces
Refactoring Techniques
1. Extract Function
// Before: Long function
function printOwing(invoice) {
let outstanding = 0;
console.log('***********************');
console.log('**** Customer Owes ****');
console.log('***********************');
for (const o of invoice.orders) {
outstanding += o.amount;
}
const today = new Date();
invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);
console.log(`name: ${invoice.customer}`);
console.log(`amount: ${outstanding}`);
console.log(`due: ${invoice.dueDate.toLocaleDateString()}`);
}
// After: Extracted functions
function printOwing(invoice) {
printBanner();
const outstanding = calculateOutstanding(invoice);
recordDueDate(invoice);
printDetails(invoice, outstanding);
}
function printBanner() {
console.log('***********************');
console.log('**** Customer Owes ****');
console.log('***********************');
}
function calculateOutstanding(invoice) {
return invoice.orders.reduce((sum, order) => sum + order.amount, 0);
}
function recordDueDate(invoice) {
const today = new Date();
invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);
}
function printDetails(invoice, outstanding) {
console.log(`name: ${invoice.customer}`);
console.log(`amount: ${outstanding}`);
console.log(`due: ${invoice.dueDate.toLocaleDateString()}`);
}
2. Extract Variable
// Before: Complex expression
if ((platform.toUpperCase().indexOf('MAC') > -1) &&
(browser.toUpperCase().indexOf('IE') > -1) &&
wasInitialized() && resize > 0) {
// Do something
}
// After: Extracted variables
const isMacOS = platform.toUpperCase().indexOf('MAC') > -1;
const isIE = browser.toUpperCase().indexOf('IE') > -1;
const wasResized = resize > 0;
if (isMacOS && isIE && wasInitialized() && wasResized) {
// Do something
}
3. Rename Variable
// Before: Unclear name
const d = new Date();
const a = users.filter(u => u.active);
// After: Clear name
const currentDate = new Date();
const activeUsers = users.filter(user => user.active);
4. Replace Magic Numbers
// Before: Magic numbers
if (status === 3) {
// What is 3?
}
// After: Named constants
const ORDER_STATUS = {
PENDING: 1,
PROCESSING: 2,
COMPLETED: 3
};
if (status === ORDER_STATUS.COMPLETED) {
// Clear intent
}
5. Replace Conditional with Polymorphism
// Before: Conditional logic
function getPrice(item) {
switch (item.type) {
case 'book':
return item.price * 0.9;
case 'electronics':
return item.price * 0.95;
default:
return item.price;
}
}
// After: Polymorphism
class Book {
getPrice() {
return this.price * 0.9;
}
}
class Electronics {
getPrice() {
return this.price * 0.95;
}
}
class Item {
getPrice() {
return this.price;
}
}
Refactoring Legacy Code
The Challenge
Legacy code often has:
- No tests
- Tight coupling
- Unclear structure
- Unknown behavior
Strategy: The Boy Scout Rule
"Leave the code cleaner than you found it."
// You're adding a feature to messy code
function processOrder(order) {
// Messy existing code
// ... 200 lines ...
// Your new feature
sendConfirmationEmail(order);
// While you're here, clean up a bit
// Extract a function, rename a variable, etc.
}
Strategy: Strangler Fig Pattern
Gradually replace old code:
// Old code (keep working)
function processOrderOld(order) {
// Old implementation
}
// New code (gradually migrate)
function processOrderNew(order) {
// New, cleaner implementation
}
// Router (switch gradually)
function processOrder(order) {
if (order.useNewFlow) {
return processOrderNew(order);
}
return processOrderOld(order);
}
Strategy: Add Tests First
// Step 1: Add tests for existing behavior
test('processOrder calculates total correctly', () => {
const order = { items: [{ price: 10, quantity: 2 }] };
const result = processOrder(order);
expect(result.total).toBe(20);
});
// Step 2: Refactor with confidence
function processOrder(order) {
// Now you can refactor safely
// Tests will catch if you break something
}
Safe Refactoring Process
1. Understand the Code
// Read the code
// Add comments
// Draw diagrams
// Ask questions
2. Add Tests
// Write tests for current behavior
// Don't change code yet
// Just understand and test
3. Make Small Changes
// One refactoring at a time
// Run tests after each change
// Commit frequently
4. Verify Behavior
// Run all tests
// Manual testing
// Code review
Common Refactoring Patterns
Extract Method
// Before
function calculateTotal(items) {
let total = 0;
for (const item of items) {
if (item.discount) {
total += item.price * (1 - item.discount);
} else {
total += item.price;
}
}
return total;
}
// After
function calculateTotal(items) {
return items.reduce((sum, item) => sum + getItemPrice(item), 0);
}
function getItemPrice(item) {
return item.discount
? item.price * (1 - item.discount)
: item.price;
}
Replace Parameter with Object
// Before: Too many parameters
function createUser(name, email, age, role, status) {
// ...
}
// After: Parameter object
function createUser(userData) {
const { name, email, age, role, status } = userData;
// ...
}
Consolidate Duplicate Code
// Before: Duplication
function calculateShippingUS(order) {
if (order.total > 100) {
return 0;
}
return 10;
}
function calculateShippingEU(order) {
if (order.total > 100) {
return 0;
}
return 15;
}
// After: Consolidated
function calculateShipping(order, region) {
if (order.total > 100) {
return 0;
}
return region === 'US' ? 10 : 15;
}
Refactoring Checklist
- Code is tested
- Tests pass before refactoring
- Small, incremental changes
- Tests pass after each change
- No behavior changes
- Code is cleaner
- Documented if needed
Real-World Example
Challenge: 500-line function, impossible to understand or test.
Refactoring Process:
- Added tests - Tested current behavior
- Extracted functions - Broke into smaller pieces
- Renamed variables - Made names clear
- Removed duplication - DRY principle
- Improved structure - Better organization
Result:
- Function length: 500 lines → 5 functions of ~20 lines each
- Test coverage: 0% → 90%
- Understandability: Much better
- Maintainability: Significantly improved
Best Practices
- Test first - Don't refactor without tests
- Small steps - One change at a time
- Run tests - After every change
- Commit often - Easy to rollback
- Understand first - Know what code does
- Improve incrementally - Don't try to fix everything
- Document decisions - Why you refactored
- Code review - Get feedback
Conclusion
Refactoring is essential for maintaining healthy codebases. The key is to:
- Refactor regularly - Don't let debt accumulate
- Test first - Safety net for changes
- Small steps - Incremental improvements
- Understand code - Know what you're changing
Remember: Good code is not written—it's refactored. Start with working code, then improve it incrementally.
What refactoring challenges have you faced? What techniques have worked best for you?
Related Posts
Managing Technical Debt: A Pragmatic Approach
Learn how to identify, prioritize, and manage technical debt effectively. A practical guide to keeping your codebase healthy without slowing down development.
Documentation Best Practices: Writing Code That Documents Itself
Learn how to write effective documentation that helps your team understand and maintain code. From code comments to API docs, master the art of clear communication.
Code Review Best Practices: How to Review Code Effectively
Master the art of code review. Learn how to provide constructive feedback, catch bugs early, and improve code quality through effective peer review.
Code Review Best Practices: A Senior Engineer's Guide
Learn how to conduct effective code reviews that improve code quality, share knowledge, and build stronger teams. Practical advice from years of reviewing code.
TypeScript Best Practices: Writing Maintainable Enterprise Code
Master TypeScript patterns and practices that make enterprise codebases maintainable, scalable, and type-safe. Learn from real-world examples and common pitfalls.
AI Security and Privacy: Building Trustworthy AI Applications
Understand critical security and privacy considerations when building AI applications. Learn about prompt injection attacks, data privacy regulations, model safety, and how to build AI systems users can trust.