正在加载,请稍候…

Refactoring Legacy Code: Safe Strategies and Techniques

Safely refactor legacy codebases without breaking functionality. Learn the Strangler Fig pattern, seam extraction, and incremental improvement strategies.

Refactoring Legacy Code: Safe Strategies and Techniques

Legacy code is code without tests—regardless of age.

The Golden Rule: Never Refactor Without Tests

// Step 1: Write characterization tests (capture current behavior)
test('UserProcessor.process - existing behavior', () => {
  const processor = new UserProcessor(legacyDb);
  const result = processor.process(testUser);
  // Record WHAT it does, not what it SHOULD do
  expect(result.status).toBe('processed');
  expect(result.notificationCount).toBe(1);
});

// Step 2: Refactor under the test's protection
// Step 3: Verify tests still pass

Strangler Fig Pattern

Gradually replace legacy system by routing new requests to new code.

// Legacy code still exists
class LegacyPaymentProcessor {
  process(order: any) { /* old implementation */ }
}

// New code grows alongside
class ModernPaymentService {
  async process(order: Order): Promise<PaymentResult> { /* new implementation */ }
}

// Facade routes between old and new
class PaymentFacade {
  constructor(
    private legacy: LegacyPaymentProcessor,
    private modern: ModernPaymentService,
    private featureFlags: FeatureFlags
  ) {}

  async process(order: Order) {
    if (this.featureFlags.isEnabled('modern-payments')) {
      return await this.modern.process(order);
    }
    return this.legacy.process(order);
  }
}

Extract Seams

Find "seams" where you can change behavior without modifying code.

// Hard to test: constructor creates dependency
class ReportGenerator {
  private emailer = new SmtpEmailer(); // seam blocked

  generate(data: ReportData) {
    const report = this.buildReport(data);
    this.emailer.send(report);
  }
}

// Extract seam through constructor injection
class ReportGenerator {
  constructor(private emailer: Emailer) {} // seam exposed

  generate(data: ReportData) {
    const report = this.buildReport(data);
    this.emailer.send(report);
  }
}

// Now testable
const generator = new ReportGenerator(new MockEmailer());

Incremental Improvement Checklist

When touching legacy code:
1. Add tests for code you're about to change
2. Extract method for complex blocks
3. Rename variables/methods to be clearer
4. Remove dead code (check git history first)
5. Extract constants for magic numbers
6. Simplify nested conditionals

Leave the campsite cleaner than you found it.

Breaking Dependencies

// Dependency on global state
class OrderService {
  save(order: Order) {
    Database.getInstance().save(order); // global singleton
  }
}

// Break the dependency
class OrderService {
  constructor(private db: Database) {}

  save(order: Order) {
    this.db.save(order);
  }
}

// In production code
const service = new OrderService(Database.getInstance());

// In tests
const service = new OrderService(new InMemoryDatabase());

Large Class Decomposition

// God class doing everything
class UserManager {
  register() { /* ... */ }
  login() { /* ... */ }
  sendEmail() { /* ... */ }
  generateReport() { /* ... */ }
  updateProfile() { /* ... */ }
  checkPermissions() { /* ... */ }
}

// Decompose by responsibility
class AuthService { register() {} login() {} }
class UserNotificationService { sendEmail() {} }
class UserReportService { generateReport() {} }
class UserProfileService { updateProfile() {} }
class AuthorizationService { checkPermissions() {} }

Refactoring is not a big-bang rewrite—it's continuous, test-driven improvement.