Back to articles

From Scratch: A Complete Guide to Refactoring Legacy Code with AI

Learn how to safely refactor old code using AI tools, including the complete process of assessment, planning, execution, and verification.

AIProgHub
December 4, 2024
4 min read
RefactoringLegacy CodeAI CodingBest Practices

The Pain of Legacy Code

Every programmer has encountered this kind of code:

// Written in 2018, no one dares touch it
function processData(d, f, t, opts) {
  var r = [];
  for (var i = 0; i < d.length; i++) {
    if (d[i].type == t || t == null) {
      var x = d[i];
      if (opts && opts.filter) {
        if (opts.filter(x)) {
          if (f) {
            r.push(f(x));
          } else {
            r.push(x);
          }
        }
      } else {
        if (f) {
          r.push(f(x));
        } else {
          r.push(x);
        }
      }
    }
  }
  return r;
}

Don't understand it? No worries, AI can help.

Pre-Refactoring Preparation

1. Assess Code Coverage

# Run tests and generate coverage report
npm test -- --coverage

If coverage is below 60%, add tests first.

2. Get AI to Explain Code

Prompt:

Analyze the following code's functionality:
1. Explain each parameter's purpose
2. Describe the function's main logic
3. List potential issues and risks

Code:
[Paste code]

AI Analysis:

This function filters and transforms data arrays:

Parameters:
- d: data array
- f: optional transform function
- t: optional type filter
- opts: config object with additional filter

Problems:
1. Parameter names too cryptic
2. Deeply nested if statements (4 levels)
3. Uses var instead of let/const
4. Duplicate code (push logic appears twice)
5. Missing error handling

3. Set Refactoring Goals

Based on AI analysis, set clear objectives:

  • [ ] Improve naming
  • [ ] Reduce nesting levels
  • [ ] Use modern JavaScript syntax
  • [ ] Add TypeScript types
  • [ ] Eliminate code duplication

Step-by-Step Refactoring Strategy

Step 1: Add Tests First

Prompt:

Generate comprehensive unit tests for this function covering:
1. Normal usage scenarios
2. Edge cases
3. Null/undefined handling
4. Various parameter combinations

Step 2: Refactor in Small Steps

Rule: Change one thing at a time

Round 1: Improve naming

function processData(data, transform, typeFilter, options) {
  var results = [];
  for (var i = 0; i < data.length; i++) {
    if (data[i].type == typeFilter || typeFilter == null) {
      var item = data[i];
      // ... rest unchanged
    }
  }
  return results;
}

Run tests → All pass → Commit

Round 2: Modernize syntax

function processData(data, transform, typeFilter, options) {
  const results = [];
  for (const item of data) {
    if (item.type === typeFilter || typeFilter === null) {
      // ... rest unchanged
    }
  }
  return results;
}

Run tests → All pass → Commit

Round 3: Reduce nesting

function processData(data, transform, typeFilter, options) {
  const results = [];

  for (const item of data) {
    if (typeFilter !== null && item.type !== typeFilter) {
      continue;
    }

    if (options?.filter && !options.filter(item)) {
      continue;
    }

    const result = transform ? transform(item) : item;
    results.push(result);
  }

  return results;
}

Step 3: Final AI-Assisted Optimization

Prompt:

Refactor this code to modern TypeScript with:
1. Complete type definitions
2. Functional programming style
3. JSDoc documentation
4. Identical functionality

Code:
[Paste previous version]

Final Version:

interface DataItem {
  type: string;
  [key: string]: unknown;
}

interface ProcessOptions<T extends DataItem> {
  filter?: (item: T) => boolean;
}

/**
 * Filter and transform data array
 *
 * @param data - Data array to process
 * @param transform - Optional transform function
 * @param typeFilter - Optional type filter, null means no filter
 * @param options - Additional configuration
 * @returns Processed data array
 */
export function processData<T extends DataItem, R = T>(
  data: T[],
  transform?: ((item: T) => R) | null,
  typeFilter?: string | null,
  options?: ProcessOptions<T>
): R[] {
  return data
    .filter(item => typeFilter === null || item.type === typeFilter)
    .filter(item => !options?.filter || options.filter(item))
    .map(item => (transform ? transform(item) : item) as R);
}

Refactoring Checklist

  • [ ] All tests pass
  • [ ] Coverage hasn't decreased
  • [ ] No significant performance degradation
  • [ ] Improved code readability
  • [ ] Team review approval

Common Pitfalls

1. Changing Too Much at Once

❌ Wrong: Refactor entire file at once ✅ Right: Change one thing, commit frequently

2. Ignoring Tests

❌ Wrong: Refactor first, add tests later ✅ Right: Add tests first, then refactor

3. Blindly Trusting AI

❌ Wrong: Use whatever AI generates ✅ Right: Always run tests for verification

Summary

AI makes legacy code refactoring manageable:

  1. Understand: Let AI explain complex code
  2. Test: Let AI generate test cases
  3. Refactor: Let AI suggest improvements
  4. Verify: Always run tests

Remember: AI is an assistant, not a replacement. You have final say.


Further Reading: AI Coding Tips | Prompt Engineering Guide

Related Articles

View all

订阅我们的邮件列表

第一时间获取最新 AI 编程教程和工具推荐

我们尊重你的隐私,不会分享你的邮箱

From Scratch: A Complete Guide to Refactoring Legacy Code with AI | AIProgHub