The Art of Code Refactoring: When and How to Refactor Legacy Code

The Art of Code Refactoring: When and How to Refactor Legacy Code

BySanjay Goraniya
3 min read
Share:

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

  1. Before adding features - Clean code makes adding features easier
  2. When fixing bugs - Clean up the area while you're there
  3. During code review - Improve code before it merges
  4. When understanding code - Refactor to understand better
  5. Regular maintenance - Scheduled refactoring time

Bad Times to Refactor

  1. During critical deadlines - Focus on delivery
  2. When code works fine - Don't fix what isn't broken
  3. Without tests - Too risky
  4. Large refactors - Break into smaller pieces

Refactoring Techniques

1. Extract Function

Code
// 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

Code
// 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

Code
// 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

Code
// 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

Code
// 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."

Code
// 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:

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

Code
// 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

Code
// Read the code
// Add comments
// Draw diagrams
// Ask questions

2. Add Tests

Code
// Write tests for current behavior
// Don't change code yet
// Just understand and test

3. Make Small Changes

Code
// One refactoring at a time
// Run tests after each change
// Commit frequently

4. Verify Behavior

Code
// Run all tests
// Manual testing
// Code review

Common Refactoring Patterns

Extract Method

Code
// 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

Code
// 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

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:

  1. Added tests - Tested current behavior
  2. Extracted functions - Broke into smaller pieces
  3. Renamed variables - Made names clear
  4. Removed duplication - DRY principle
  5. 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

  1. Test first - Don't refactor without tests
  2. Small steps - One change at a time
  3. Run tests - After every change
  4. Commit often - Easy to rollback
  5. Understand first - Know what code does
  6. Improve incrementally - Don't try to fix everything
  7. Document decisions - Why you refactored
  8. 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?

Share:

Related Posts