Lesson 47 of 54

Refactoring Legacy Code

Safe Refactoring Techniques

A teammate once "refactored" a payment module and broke production. The code looked cleaner. The tests passed. But a subtle behavior change—rounding in a different direction—cost the company thousands in incorrect charges.

Refactoring means changing structure without changing behavior. If behavior changes, it's not refactoring. It's a feature or a bug.

The techniques in this lesson are your safety toolkit. Small, reversible steps. One at a time.

The Golden Rule: One Refactoring at a Time

Never mix refactoring with feature work. Never do five refactorings in one commit.

Why? When something breaks, you need to know which change caused it. One refactoring = one commit = easy rollback.

Javascript
// BAD: Extracting, renaming, and inlining in one go
// GOOD: Extract method. Commit. Rename. Commit. Inline. Commit.

Extract Method / Function

The most powerful refactoring. Take a block of code that does one thing and give it a name.

Before

Javascript
function processOrder(order) {
  // Validate
  if (!order.items?.length) throw new Error('Empty order');
  if (!order.customer?.email) throw new Error('Missing email');
  if (order.total < 0) throw new Error('Invalid total');

  // Calculate
  const subtotal = order.items.reduce((sum, i) => sum + i.price * i.quantity, 0);
  const tax = subtotal * 0.08;
  const shipping = order.items.length > 5 ? 15 : 8;

  // Process
  chargePayment(order.paymentMethod, subtotal + tax + shipping);
  sendConfirmation(order.customer.email, order);
  updateInventory(order.items);
}

After

Javascript
function processOrder(order) {
  validateOrder(order);
  const total = calculateOrderTotal(order);
  chargeAndFulfill(order, total);
}

function validateOrder(order) {
  if (!order.items?.length) throw new Error('Empty order');
  if (!order.customer?.email) throw new Error('Missing email');
  if (order.total < 0) throw new Error('Invalid total');
}

function calculateOrderTotal(order) {
  const subtotal = order.items.reduce((sum, i) => sum + i.price * i.quantity, 0);
  const tax = subtotal * 0.08;
  const shipping = order.items.length > 5 ? 15 : 8;
  return subtotal + tax + shipping;
}

function chargeAndFulfill(order, total) {
  chargePayment(order.paymentMethod, total);
  sendConfirmation(order.customer.email, order);
  updateInventory(order.items);
}

Each extracted function has a single responsibility. The main function reads like a table of contents.

When to extract: When a block of code has a clear purpose you can name. When you'd add a comment to explain it—extract instead.

Extract Variable

Give a complex expression a name. Improves readability and often reveals intent.

Before

Javascript
if (user.age >= 18 && user.verified && !user.suspended && user.subscription?.status === 'active') {
  showPremiumContent();
}

After

Javascript
const isEligibleForPremium =
  user.age >= 18 &&
  user.verified &&
  !user.suspended &&
  user.subscription?.status === 'active';

if (isEligibleForPremium) {
  showPremiumContent();
}

The variable name documents the business rule. The condition becomes self-explanatory.

In TypeScript, extracted variables also help type narrowing:

Typescript
const order = getOrder(id);
if (order && order.status === 'shipped' && order.trackingNumber) {
  // TypeScript knows order has trackingNumber here
  displayTracking(order.trackingNumber);
}

Rename

Low risk. High value. Do it often.

Poor names are the most common code smell. Renaming is the cheapest fix.

Javascript
// Before: What does "d" mean?
function calc(d) {
  return d * 1.1;
}

// After: Intent is clear
function calculatePriceWithTax(priceBeforeTax) {
  return priceBeforeTax * 1.1;
}

Use your IDE. Modern editors (VS Code, WebStorm) rename across the entire codebase safely. Don't do it manually—you'll miss references.

Rename in small steps: If a variable is used in 20 places, rename it. The IDE updates all 20. One commit.

Inline

The opposite of extract. Remove indirection when it adds no value.

Sometimes we over-extract. A one-line function that's only called once might not earn its name.

Before

Javascript
function getFullName(user) {
  return `${user.firstName} ${user.lastName}`;
}

function displayUser(user) {
  const name = getFullName(user);
  return `<h1>${name}</h1>`;
}

After (when used once)

Javascript
function displayUser(user) {
  return `<h1>${user.firstName} ${user.lastName}</h1>`;
}

When to inline: When the extracted function is trivial, used once, and the name doesn't add clarity. When a wrapper exists "for flexibility" but nobody ever needs that flexibility.

When not to inline: When the extracted name carries important domain meaning. When the logic is reused. When inlining would make the caller harder to read.

Move Method

Put the method on the class that has the data it needs.

Feature Envy (from the previous lesson) is fixed by moving the method.

Before

Javascript
class Order {
  constructor(public items: LineItem[], public customer: Customer) {}
}

class InvoiceGenerator {
  generate(order: Order): string {
    // This method uses order's data more than its own
    const subtotal = order.items.reduce((sum, i) => sum + i.price * i.quantity, 0);
    const tax = subtotal * 0.08;
    return `Total: $${(subtotal + tax).toFixed(2)}`;
  }
}

After

Javascript
class Order {
  constructor(public items: LineItem[], public customer: Customer) {}

  calculateTotal(): number {
    const subtotal = this.items.reduce((sum, i) => sum + i.price * i.quantity, 0);
    const tax = subtotal * 0.08;
    return subtotal + tax;
  }
}

class InvoiceGenerator {
  generate(order: Order): string {
    return `Total: $${order.calculateTotal().toFixed(2)}`;
  }
}

The calculation lives with the data. InvoiceGenerator orchestrates; Order owns its totals.

IDE Refactoring Tools

Your IDE does the mechanical work. Use it.

RefactoringVS Code / CursorWebStorm
Extract functionSelect → Refactor → ExtractCtrl+Alt+M
Extract variableSelect → Refactor → ExtractCtrl+Alt+V
Rename symbolF2Shift+F6
InlineRight-click → InlineCtrl+Alt+N

Why use the IDE: It updates all references. It handles edge cases (imports, re-exports, type definitions). It's faster and safer than manual editing.

When the IDE fails: Complex cases—dynamic property access, reflection, string-based lookups. Then refactor manually, but still one step at a time.

The Refactoring Workflow

  1. Ensure tests pass before you start.
  2. Make one small change. Extract one method. Rename one variable.
  3. Run tests. If they fail, you broke something. Fix it before continuing.
  4. Commit. Small, atomic commits. "Extract validateOrder" is a good commit message.
  5. Repeat.

This loop—change, test, commit—is the safety net. It makes refactoring predictable.

When Refactoring Feels Risky

If you're nervous, the code probably lacks tests. That's a different problem (covered in the next lesson).

If you have tests and you're still nervous:

  • Make the change smaller
  • Commit more frequently
  • Use the IDE's refactoring tools—they're battle-tested

Key insight: Refactoring = structure changes, behavior stays the same. One refactoring at a time. Extract, rename, inline, move. Use your IDE. Test after each step. Commit often.