Je kent hem wel. Die ene service class die klein en overzichtelijk begon, en toen maar bleef groeien. Sprint na sprint, feature na feature. Tot je hem op een dag opent en denkt: dit ding is 800 regels en doet alles.

Bij mij was het OrderProcessingService.cs. Het valideerde orders, berekende prijzen, beheerde statussen, verstuurde notificaties, en maakte waarschijnlijk ook nog koffie. Het stond al weken op de refactoring-backlog. Niemand wilde eraan.


Waarom handmatig refactoren zo vervelend is

Een grote service opsplitsen is technisch niet moeilijk. Je weet wat er moet gebeuren: verantwoordelijkheden identificeren, nieuwe classes maken, logica extraheren, dependency injection doorvoeren, en tests aanpassen.

Maar de uitvoering is waar het vervelend wordt. Je moet zorgvuldig uitzoeken welke methodes afhankelijk zijn van welke private fields. Je moet bepalen welke groepen methodes bij elkaar horen. En je moet er zeker van zijn dat je niks breekt — en in een bestand van 800 regels zijn er genoeg plekken waar het stilletjes mis kan gaan.

Het is het soort taak waarbij je denkt “dit kost me een uur” en dan is het 16:00 en zit je nog steeds compileerfouten op te lossen.


Stap 1: Vraag om een analyse

Ik opende mijn terminal in het project en startte Claude Code. Eerste vraag:

“Analyseer OrderProcessingService.cs. Welke afzonderlijke verantwoordelijkheden heeft deze class?”

Claude Code opende het bestand, las alle 800 regels, en kwam terug met een overzicht:

  1. Ordervalidatie — voorraad checken, adressen valideren, kredietlimieten controleren
  2. Prijsberekening — basisprijzen, kortingen, belasting, verzendkosten
  3. Statusbeheer — statusovergangen, statushistorie, notificatie-triggers
  4. Notificatieverzending — e-mails, webhook-calls naar externe systemen

Vier duidelijke verantwoordelijkheden in één class. Ik had al een vermoeden, maar het concreet zien met specifieke methodenamen per groep maakte het tastbaar.


Stap 2: Eerst plannen, dan knippen

Ik wilde niet dat Claude Code meteen begon met hakken. Dus schakelde ik over naar Plan Mode (Shift+Tab) en vroeg:

“Maak een plan om deze service op te splitsen volgens het Single Responsibility Principle. Houd de originele service als orchestrator.”

Claude Code stelde voor:

  • IOrderValidator + OrderValidator — alle validatielogica
  • IPriceCalculator + PriceCalculator — prijs- en kortingslogica
  • IOrderStatusManager + OrderStatusManager — statusovergangen en historie
  • OrderProcessingService behouden als dunne orchestrator die de drie aanstuurt

Goed plan. Akkoord.


Stap 3: Claude Code voert het uit

Hier wordt het interessant. Claude Code verplaatste niet zomaar wat methodes. Het:

  • Maakte de interfaces aan met de juiste method signatures
  • Extraheerde de implementaties, inclusief de private helper-methodes die elke groep nodig had
  • Paste OrderProcessingService aan om de nieuwe dependencies te injecteren
  • Registreerde alles in Program.cs

De originele service zag er ongeveer zo uit (vereenvoudigd):

public class OrderProcessingService
{
    private readonly AppDbContext _db;
    private readonly ILogger<OrderProcessingService> _logger;

    public OrderProcessingService(AppDbContext db, ILogger<OrderProcessingService> logger)
    {
        _db = db;
        _logger = logger;
    }

    public async Task<OrderResult> ProcessOrderAsync(Order order)
    {
        // Validatie (150+ regels logica)
        if (!await HasSufficientStock(order))
            return OrderResult.Failed("Insufficient stock");
        if (!IsValidShippingAddress(order.ShippingAddress))
            return OrderResult.Failed("Invalid address");
        if (!await CheckCreditLimit(order.Customer, order.Total))
            return OrderResult.Failed("Credit limit exceeded");

        // Prijsberekening (200+ regels logica)
        var basePrice = CalculateBasePrice(order.Items);
        var discount = ApplyDiscounts(order.Customer, basePrice);
        var tax = CalculateTax(order.ShippingAddress, discount);
        var shipping = CalculateShipping(order.Items, order.ShippingAddress);
        order.Total = discount + tax + shipping;

        // Statusbeheer (100+ regels logica)
        order.Status = OrderStatus.Confirmed;
        await RecordStatusChange(order, OrderStatus.Confirmed);
        await NotifyCustomer(order);

        await _db.SaveChangesAsync();
        return OrderResult.Success(order);
    }

    // ... 20+ private methodes per verantwoordelijkheid
}

Na de refactor waren de interfaces strak en gefocust:

public interface IOrderValidator
{
    Task<ValidationResult> ValidateOrderAsync(Order order);
}

public interface IPriceCalculator
{
    PriceBreakdown CalculatePrice(IReadOnlyList<OrderItem> items,
        Customer customer, Address shippingAddress);
}

public interface IOrderStatusManager
{
    Task TransitionStatusAsync(Order order, OrderStatus newStatus);
}

En de orchestrerende service werd overzichtelijk:

public class OrderProcessingService
{
    private readonly IOrderValidator _validator;
    private readonly IPriceCalculator _calculator;
    private readonly IOrderStatusManager _statusManager;
    private readonly AppDbContext _db;

    public OrderProcessingService(
        IOrderValidator validator,
        IPriceCalculator calculator,
        IOrderStatusManager statusManager,
        AppDbContext db)
    {
        _validator = validator;
        _calculator = calculator;
        _statusManager = statusManager;
        _db = db;
    }

    public async Task<OrderResult> ProcessOrderAsync(Order order)
    {
        var validation = await _validator.ValidateOrderAsync(order);
        if (!validation.IsValid)
            return OrderResult.Failed(validation.ErrorMessage);

        var pricing = _calculator.CalculatePrice(
            order.Items, order.Customer, order.ShippingAddress);
        order.Total = pricing.Total;

        await _statusManager.TransitionStatusAsync(order, OrderStatus.Confirmed);

        await _db.SaveChangesAsync();
        return OrderResult.Success(order);
    }
}

Van 800 regels naar zo’n 30. De logica is niet verdwenen — die zit nu waar hij hoort.


Stap 4: DI-registratie en verificatie

Claude Code paste ook Program.cs aan:

builder.Services.AddScoped<IOrderValidator, OrderValidator>();
builder.Services.AddScoped<IPriceCalculator, PriceCalculator>();
builder.Services.AddScoped<IOrderStatusManager, OrderStatusManager>();
builder.Services.AddScoped<OrderProcessingService>();

Daarna draaide het dotnet build. Compileert netjes. Dan dotnet test. Alle 47 bestaande tests slaagden. Geen regressies.

Het hele traject kostte ongeveer 40 minuten, inclusief review. Dat backlog-item waar niemand aan wilde beginnen? Klaar voor de lunch.


Wanneer dit goed werkt — en wanneer niet

Dit soort refactoring is waar Claude Code uitblinkt. De verantwoordelijkheden waren duidelijk te scheiden. Validatielogica hoeft niks te weten over prijsberekeningen. Statusbeheer heeft niks te maken met verzendkosten. Heldere grenzen, heldere splitsingen.

Het wordt lastiger als de verantwoordelijkheden verweven zijn. Als je validatieregels afhangen van de berekende prijs, die afhangt van de klantstatus, die weer wordt bijgewerkt tijdens de validatie — dan is het een ander verhaal. Claude Code helpt je er nog steeds doorheen, maar de splitsing wordt minder strak en je moet meer zelf beslissen waar je de grenzen trekt.

Mijn advies: begin met de makkelijke winst. De service met vier duidelijke verantwoordelijkheden waar iedereen het over eens is dat ze apart horen. Bouw vertrouwen op met die gevallen voordat je de lastige aanpakt.


Tips voor je eigen refactoring

  1. Begin altijd met analyse. Spring niet direct naar “refactor dit.” Vraag Claude Code eerst om de verantwoordelijkheden te identificeren. Het ziet vaak groeperingen die jij over het hoofd had gezien.

  2. Gebruik Plan Mode. Een refactoring-plan dat je kunt reviewen voor de uitvoering bespaart je het terugdraaien van slechte splitsingen. Shift+Tab is je vriend.

  3. Behoud de orchestrator. Verwijder de originele class niet. Maak er een dunne coordinator van. Zo blijven bestaande aanroepers werken en beperk je de impact van de wijziging.

  4. Draai de tests na elke stap. Claude Code kan dit voor je doen. Een dotnet test na elke geextraheerde class vangt problemen vroeg op, als ze nog makkelijk te fixen zijn.

  5. Review de interfaces. De method signatures die Claude Code maakt zijn meestal logisch, maar jij kent je domein beter. Als een interface niet goed voelt, zeg het — Claude Code past het aan.


Probeer het zelf

Heb je een service die ongecontroleerd is gegroeid? Open je terminal:

claude

“Analyseer [JouwService].cs. Welke verantwoordelijkheden heeft deze class? Stel een plan voor om het op te splitsen.”

Je zult verrast zijn hoe snel dat backlog-item gaat van “niemand wil eraan” naar “klaar.”