Przejdź do głównej zawartości

Large-Scale Refactoring

Ta treść nie jest jeszcze dostępna w Twoim języku.

Refactoring is the art of improving code structure without changing its behavior. It’s where technical debt meets its reckoning, where legacy systems get a second life, and where Claude Code truly shines. This lesson explores how to leverage AI assistance for large-scale refactoring that would take months manually.

Scenario: You’ve inherited a 5-year-old e-commerce platform. 200,000 lines of code. jQuery spaghetti in the frontend. PHP 5.6 in the backend. No tests. Inconsistent patterns. The business wants to add real-time features, but touching anything risks breaking everything. Sound familiar?

Month 1: Analysis and planning
- Manual code review
- Document all dependencies
- Create refactoring roadmap
Month 2-3: Write tests for critical paths
- Manually write test cases
- Hope you caught edge cases
Month 4-6: Incremental refactoring
- Update one module at a time
- Fix breaks as they appear
- Pray nothing slips through
Month 7+: Ongoing fixes and regression
  1. Create a safety net

    > Before we start refactoring:
    > 1. Create a comprehensive test suite for current behavior
    > 2. Set up git hooks for pre-commit testing
    > 3. Configure CI/CD to catch regressions
    > 4. Document current behavior
  2. Analyze the current state

    > Analyze our codebase and create a refactoring plan:
    > - Identify code smells
    > - Find duplicate code
    > - Map dependencies
    > - List deprecated patterns
    > - Estimate complexity for each module
  3. Set up refactoring infrastructure Create .claude/commands/refactor-safely.md:

    Refactor $ARGUMENTS following these rules:
    1. Maintain exact functionality
    2. Write tests before changing code
    3. Update one pattern at a time
    4. Run tests after each change
    5. Create descriptive commits
    6. Update documentation
  4. Configure incremental approach

    {
    "refactoring": {
    "batchSize": 10,
    "testFirst": true,
    "preserveComments": true,
    "updateDocs": true,
    "commitAfterEach": true
    }
    }

From jQuery chaos to modern React/Vue/Angular

> Analyze our jQuery codebase and create a modernization plan.
> We want to migrate to React gradually while keeping the app functional.

Claude’s incremental strategy:

Phase 1: Preparation

// 1. Create abstraction layer
class LegacyBridge {
static $(selector) {
// Wrap jQuery for gradual replacement
return {
original: $(selector),
onClick: (handler) => {
$(selector).on('click', handler);
// Track for migration
this.trackUsage('click', selector);
}
};
}
}
// 2. Replace direct jQuery calls
// Before: $('#button').click(handler)
// After: LegacyBridge.$('#button').onClick(handler)

Phase 2: Component Extraction

// Identify UI components in jQuery
> Find all jQuery UI patterns and extract them into components
// Claude identifies patterns like:
$('.user-card').each(function() {
$(this).find('.name').text(userData.name);
$(this).find('.email').text(userData.email);
});
// Converts to React component:
const UserCard = ({ user }) => (
<div className="user-card">
<div className="name">{user.name}</div>
<div className="email">{user.email}</div>
</div>
);

Phase 3: State Management

// From scattered jQuery state
var userLoggedIn = false;
$('#login').click(function() {
userLoggedIn = true;
updateUI();
});
// To centralized state
const useAuthStore = create((set) => ({
isLoggedIn: false,
login: () => set({ isLoggedIn: true }),
logout: () => set({ isLoggedIn: false })
}));

Phase 4: Complete Migration

// Final cleanup
> Remove jQuery dependencies
> Update build system
> Remove legacy bridge code
> Optimize bundle size
// Result: Modern React app
// 60% smaller bundle
// 3x faster rendering
// Type-safe with TypeScript

From PHP 5.6 spaghetti to modern PHP 8+ or Node.js

> Our backend is PHP 5.6 with:
> - No namespace organization
> - Mixed HTML/PHP files
> - Direct MySQL queries (no ORM)
> - Global variables everywhere
>
> Create a plan to modernize to PHP 8.3 with Laravel

Claude’s systematic approach:

// Before: PHP 5.6 style
function calculateTotal($items, $tax) {
$total = 0;
foreach ($items as $item) {
$total += $item['price'] * $item['quantity'];
}
return $total + ($total * $tax);
}
// After: PHP 8.3 with types
declare(strict_types=1);
function calculateTotal(
array $items,
float $tax
): float {
$subtotal = array_reduce(
$items,
fn($carry, $item) => $carry + ($item->price * $item->quantity),
0.0
);
return $subtotal * (1 + $tax);
}

Refactoring database structure without downtime

> We need to refactor our database:
> - users table has 80 columns (denormalized)
> - No foreign keys or constraints
> - Inconsistent naming (userId vs user_id)
> - No indexes except primary keys
>
> Plan a gradual migration to normalized schema
  1. Create migration strategy

    -- Phase 1: Add new normalized tables alongside old
    CREATE TABLE user_profiles (
    id BIGINT PRIMARY KEY,
    user_id BIGINT NOT NULL,
    first_name VARCHAR(100),
    last_name VARCHAR(100),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id)
    );
    CREATE TABLE user_addresses (
    id BIGINT PRIMARY KEY,
    user_id BIGINT NOT NULL,
    type ENUM('billing', 'shipping'),
    street VARCHAR(255),
    city VARCHAR(100),
    -- ... more fields
    FOREIGN KEY (user_id) REFERENCES users(id),
    INDEX idx_user_type (user_id, type)
    );
  2. Implement dual-write pattern

    class UserService {
    public function updateProfile($userId, $data) {
    // Write to both old and new structure
    DB::transaction(function() use ($userId, $data) {
    // Legacy write
    DB::table('users')->where('id', $userId)->update([
    'fname' => $data['first_name'],
    'lname' => $data['last_name']
    ]);
    // New structure write
    UserProfile::updateOrCreate(
    ['user_id' => $userId],
    [
    'first_name' => $data['first_name'],
    'last_name' => $data['last_name']
    ]
    );
    });
    }
    }
  3. Migrate data progressively

    > Create a background job to migrate user data:
    > - Process in batches of 1000
    > - Track progress
    > - Verify data integrity
    > - Handle failures gracefully
  4. Switch reads to new schema

    // Feature flag controlled
    if (Feature::enabled('use_normalized_schema')) {
    return User::with(['profile', 'addresses'])->find($id);
    } else {
    return DB::table('users')->where('id', $id)->first();
    }
  5. Complete the migration

    -- After verification, drop old columns
    ALTER TABLE users
    DROP COLUMN fname,
    DROP COLUMN lname,
    DROP COLUMN address1,
    -- ... etc

Challenge: Extract services from a monolithic Rails application

> Our Rails monolith handles:
> - User authentication
> - Product catalog
> - Order processing
> - Email notifications
> - Reporting
>
> Plan extraction of order processing into microservice

Claude’s extraction strategy:

# Claude analyzes and finds:
> Analyzing order processing boundaries...
Found order-related code in:
- app/models/order.rb (and 12 related models)
- app/controllers/orders_controller.rb (and 4 related)
- app/services/order_service.rb (and 8 related)
- lib/payment_gateway.rb
- app/jobs/order_notification_job.rb
Shared dependencies:
- User model (for customer info)
- Product model (for catalog)
- Notification service

Challenge: Convert synchronous operations to event-driven architecture

> Our system does everything synchronously:
> - Send email after order
> - Update inventory immediately
> - Calculate analytics in-request
> - Generate PDFs during API call
>
> Refactor to async event-driven pattern

Claude implements event-driven refactoring:

// Before: Synchronous coupling
async function createOrder(orderData: OrderData) {
const order = await db.orders.create(orderData);
// All of these block the response
await sendOrderConfirmationEmail(order);
await updateInventory(order.items);
await calculateAnalytics(order);
await generateInvoicePDF(order);
return order;
}
// After: Event-driven
async function createOrder(orderData: OrderData) {
const order = await db.orders.create(orderData);
// Publish event and return immediately
await eventBus.publish('order.created', {
orderId: order.id,
customerId: order.customerId,
items: order.items,
total: order.total
});
return order;
}
// Separate handlers process async
eventBus.subscribe('order.created', async (event) => {
await Promise.all([
emailService.sendOrderConfirmation(event),
inventoryService.updateStock(event.items),
analyticsService.recordOrder(event),
pdfService.generateInvoice(event.orderId)
]);
});
> Scan our codebase for anti-patterns and create fixes:
> - God objects (classes doing too much)
> - Shotgun surgery (changes require many edits)
> - Feature envy (methods using other class data)
> - Data clumps (same parameters everywhere)

Claude’s pattern detection and fixes:

God Object Fix

// Detected: UserService with 47 methods
class UserService {
// Authentication methods
login() {}
logout() {}
resetPassword() {}
// Profile methods
updateProfile() {}
uploadAvatar() {}
// Notification methods
sendEmail() {}
sendSMS() {}
// ... 40 more methods
}
// Refactored into focused services
class AuthService {
login() {}
logout() {}
resetPassword() {}
}
class ProfileService {
updateProfile() {}
uploadAvatar() {}
}
class NotificationService {
sendEmail() {}
sendSMS() {}
}

Data Clump Fix

// Detected: Same 4 parameters in 15 methods
function createInvoice(
street: string,
city: string,
state: string,
zip: string,
// ... other params
) {}
function shipOrder(
street: string,
city: string,
state: string,
zip: string,
// ... other params
) {}
// Refactored with value object
class Address {
constructor(
public street: string,
public city: string,
public state: string,
public zip: string
) {}
}
function createInvoice(
address: Address,
// ... other params
) {}
function shipOrder(
address: Address,
// ... other params
) {}

For replacing legacy systems gradually:

> Implement strangler fig pattern for our legacy order system:
> - Current: Monolithic order processing
> - Target: Modern microservices architecture
> - Requirement: Zero downtime, gradual migration
  1. Create facade

    class OrderFacade {
    async createOrder(data: OrderData) {
    if (await this.shouldUseNewSystem(data)) {
    return this.newOrderService.create(data);
    }
    return this.legacyOrderService.create(data);
    }
    private async shouldUseNewSystem(data: OrderData) {
    // Gradual rollout logic
    if (this.featureFlags.isEnabled('new_order_system', data.customerId)) {
    return true;
    }
    if (data.total < 100 && Math.random() < 0.1) { // 10% of small orders
    return true;
    }
    return false;
    }
    }
  2. Implement comparison mode

    // Run both systems and compare
    class OrderComparison {
    async createAndCompare(data: OrderData) {
    const [legacy, modern] = await Promise.all([
    this.legacySystem.create(data),
    this.modernSystem.create(data)
    ]);
    const differences = this.compareResults(legacy, modern);
    if (differences.length > 0) {
    await this.logDifferences(differences);
    }
    // Return legacy result but log modern
    return legacy;
    }
    }
  3. Gradual cutover

    Week 1: 1% traffic to new system
    Week 2: 10% traffic
    Week 3: 50% traffic
    Week 4: 90% traffic
    Week 5: 100% traffic
    Week 6: Remove legacy code
> Create a refactoring dashboard that tracks:
> - Code coverage improvement
> - Cyclomatic complexity reduction
> - Performance improvements
> - Bug reduction
> - Technical debt score

Claude generates tracking code:

class RefactoringMetrics {
async analyze() {
const metrics = {
coverage: await this.getTestCoverage(),
complexity: await this.getCyclomaticComplexity(),
performance: await this.getPerformanceMetrics(),
codeQuality: await this.getCodeQualityScore(),
technicalDebt: await this.calculateTechnicalDebt()
};
return {
...metrics,
improvements: this.compareWithBaseline(metrics),
roi: this.calculateROI(metrics)
};
}
async getTestCoverage() {
const coverage = await exec('npm run coverage -- --json');
return {
statements: coverage.total.statements.pct,
branches: coverage.total.branches.pct,
functions: coverage.total.functions.pct,
lines: coverage.total.lines.pct
};
}
}
> Generate comprehensive tests to ensure refactoring doesn't change behavior:
> - Capture current behavior
> - Create golden master tests
> - Property-based testing
> - Regression test suite

Best Practices for Large-Scale Refactoring

Section titled “Best Practices for Large-Scale Refactoring”
> For each refactoring:
> 1. Create feature branch
> 2. Make incremental commits
> 3. Run tests continuously
> 4. Use pull requests for review
> While refactoring, also:
> - Update code comments
> - Document new patterns
> - Create migration guides
> - Record decision rationale
> When changing APIs:
> - Add new endpoints alongside old
> - Deprecate gradually
> - Provide migration tools
> - Support both versions temporarily
if (Feature.enabled('new_payment_system')) {
return this.newPaymentProcessor.charge(amount);
} else {
return this.legacyPaymentGateway.process(amount);
}

You’ve learned how to leverage Claude Code for large-scale refactoring that would be impossibly time-consuming manually. The key is thinking systematically: analyze comprehensively, refactor incrementally, validate continuously.

Remember: Refactoring is not about perfection - it’s about continuous improvement. Use Claude Code to handle the mechanical transformations while you focus on architectural decisions and business value. With AI assistance, you can tackle technical debt that’s been accumulating for years and transform legacy systems into modern, maintainable codebases.