Methods

Um método bem escrito responde a uma pergunta só, e responde no nível certo de detalhe. O método público de entrada, o orquestrador, funciona como o sumário da operação: ele nomeia os passos e não mostra como cada passo acontece. Logo abaixo dele ficam os helpers (métodos auxiliares privados), na mesma ordem em que foram chamados, para o leitor descer ao detalhe só quando quiser.

Conceitos fundamentais

ConceitoO que é
orchestrator (orquestrador)Método público de entrada que descreve o fluxo em alto nível; chama helpers em sequência
helper (método auxiliar)Método privado abaixo do orquestrador, com responsabilidade única e nome de domínio
SLA (Single Level of Abstraction · Único Nível de Abstração)Cada método opera num único nível; misturar passos altos e baixos prejudica leitura
single responsibility (responsabilidade única)Um método faz uma coisa; o nome descreve essa coisa por completo
side effect (efeito colateral)Alteração de estado externo, I/O, log; deixar explícito no nome ou na assinatura
pure function (função pura)Método sem side effects; saída depende só dos argumentos; mais fácil de testar
expression-bodied member (membro com corpo de expressão)Sintaxe => para métodos curtos com retorno único

Orquestrador no topo

O método de entrada conta o que a operação faz, na sequência em que ela acontece: valida o pedido, busca o produto, grava, notifica, monta a nota. Cada um desses passos vira uma chamada com nome próprio, e a implementação de cada um fica logo abaixo. Quem abre o arquivo entende a operação inteira lendo as primeiras vinte linhas, e desce ao detalhe do passo que interessa.

❌ Ruim: implementação misturada com orquestração
public async Task<Result<Invoice>> ProcessOrderAsync(OrderRequest request, CancellationToken ct)
{
    if (string.IsNullOrWhiteSpace(request.ProductId))
        return Result<Invoice>.Fail("Product ID required.", "INVALID_PRODUCT_ID");

    var product = await _products.FindByIdAsync(request.ProductId, ct);

    if (product is null)
        return Result<Invoice>.Fail("Product not found.", "NOT_FOUND");

    var order = new Order(request.ProductId, request.Quantity, product.Price * request.Quantity);

    await _orders.SaveAsync(order, ct);
    await _notifications.SendAsync(new OrderCreatedEvent(order.Id), ct);

    var invoice = new Invoice(order.Id, order.Total, DateTime.UtcNow);

    return Result<Invoice>.Success(invoice);
}
✅ Bom: orquestrador declara o fluxo, helpers implementam cada passo
public async Task<Result<Invoice>> ProcessOrderAsync(OrderRequest request, CancellationToken ct)
{
    var validationResult = ValidateRequest(request);
    if (validationResult.IsFailure)
        return Result<Invoice>.Fail(validationResult.Error!.Message, validationResult.Error.Code);

    var product = await _products.FindByIdAsync(request.ProductId, ct);

    if (product is null)
        return Result<Invoice>.Fail("Product not found.", "NOT_FOUND");

    var order = await SaveOrderAsync(request, product, ct);
    await NotifyOrderCreatedAsync(order, ct);

    var invoice = BuildInvoice(order);

    return Result<Invoice>.Success(invoice);
}

private static Result ValidateRequest(OrderRequest request) { ... }
private async Task<Order> SaveOrderAsync(OrderRequest request, Product product, CancellationToken ct) { ... }

private async Task NotifyOrderCreatedAsync(Order order, CancellationToken ct) { ... }
private static Invoice BuildInvoice(Order order) { ... }

Um nível de abstração por método

Cada método escolhe um papel: ou ele coordena chamadas com nome, ou ele executa um passo concreto. O método que faz as duas coisas mistura a altura da leitura. BuildOrderSummaryAsync chama o repositório, o que é alto nível, e no meio calcula imposto multiplicando por 0.1m, o que é detalhe de cálculo. Quem lê troca de altura no meio do método e perde o fio da operação. Extraia o cálculo para um helper com nome (CalculateTotals) e o método de entrada volta a ser só a sequência de passos.

❌ Ruim: orquestração e implementação no mesmo método
public async Task<OrderSummary> BuildOrderSummaryAsync(Guid orderId, CancellationToken ct)
{
    var order = await _orders.FindByIdAsync(orderId, ct);

    var subtotal = order.Items.Sum(item => item.Price * item.Quantity);
    var tax = subtotal * 0.1m;

    var total = subtotal + tax;

    var lines = order.Items.Select(item => $"{item.Name} x{item.Quantity}").ToList();
    var summary = new OrderSummary(order.Id, lines, subtotal, tax, total);

    return summary;
}
✅ Bom: orquestrador chama helpers, cada um com uma responsabilidade
public async Task<OrderSummary> BuildOrderSummaryAsync(Guid orderId, CancellationToken ct)
{
    var order = await _orders.FindByIdAsync(orderId, ct);

    var totals = CalculateTotals(order);
    var summary = BuildSummary(order, totals);
    return summary;
}

private static OrderTotals CalculateTotals(Order order)
{
    var subtotal = order.Items.Sum(item => item.Price * item.Quantity);
    var tax = subtotal * 0.1m;

    var total = subtotal + tax;
    var totals = new OrderTotals(subtotal, tax, total);
    return totals;
}

private static OrderSummary BuildSummary(Order order, OrderTotals totals)
{
    var lines = order.Items.Select(item => $"{item.Name} x{item.Quantity}").ToList();
    var summary = new OrderSummary(order.Id, lines, totals.Subtotal, totals.Tax, totals.Total);
    return summary;
}

Sem lógica no retorno

O return anuncia o que sai do método. Quando ele também monta o objeto, soma a lista e formata o texto, a última linha vira a mais densa do arquivo, justo onde o leitor esperava a resposta. Guarde o resultado numa variável com nome antes de devolvê-lo. O nome dessa variável é o que documenta o retorno, e o return volta a ter uma palavra só.

Vale também para o retorno que só repassa a chamada de outro objeto. => await _repository.FindByStatusAsync(...) devolve algo sem nunca nomear o que é. Uma linha a mais, com var pendingOrders = ..., e o leitor sabe o que sai sem consultar a assinatura.

❌ Ruim: lógica e construção inline no return
public OrderSummary BuildSummary(Order order) =>
    new OrderSummary(
        order.Id,
        order.Items.Select(i => $"{i.Name} x{i.Quantity}").ToList(),
        order.Items.Sum(i => i.Price * i.Quantity)
    );
❌ Ruim: bare return: pass-through sem nome, o retorno não diz o que é
public async Task<IEnumerable<Order>> FindPendingOrdersAsync(Guid userId, CancellationToken ct)
    => await _repository.FindByStatusAsync(userId, OrderStatus.Pending, ct);

public async Task<Invoice> ProcessCheckoutAsync(Guid cartId, CancellationToken ct)
    => await _checkoutService.ProcessAsync(cartId, ct);
❌ Ruim: string imensa montada inline: ilegível e sem semântica
public string BuildShippingLabel(Order order) =>
    $"{order.Customer.FirstName} {order.Customer.LastName}\n{order.Address.Street}, {order.Address.Number}\n{order.Address.City} - {order.Address.State}, {order.Address.ZipCode}\nOrder #{order.Id}";
✅ Bom: variável expressiva antes do return
public OrderSummary BuildSummary(Order order)
{
    var lines = order.Items.Select(item => $"{item.Name} x{item.Quantity}").ToList();
    var total = order.Items.Sum(item => item.Price * item.Quantity);

    var summary = new OrderSummary(order.Id, lines, total);
    return summary;
}
✅ Bom: nome simétrico com o método deixa claro o que sai
public async Task<IEnumerable<Order>> FindPendingOrdersAsync(Guid userId, CancellationToken ct)
{
    var pendingOrders = await _repository.FindByStatusAsync(userId, OrderStatus.Pending, ct);
    return pendingOrders;
}

public async Task<Invoice> ProcessCheckoutAsync(Guid cartId, CancellationToken ct)
{
    var invoice = await _checkoutService.ProcessAsync(cartId, ct);
    return invoice;
}
✅ Bom: partes nomeadas antes de montar o resultado
public string BuildShippingLabel(Order order)
{
    var fullName = $"{order.Customer.FirstName} {order.Customer.LastName}";
    var addressLine = $"{order.Address.Street}, {order.Address.Number}";

    var cityLine = $"{order.Address.City} - {order.Address.State}, {order.Address.ZipCode}";
    var label = $"{fullName}\n{addressLine}\n{cityLine}\nOrder #{order.Id}";
    return label;
}

Construtor primário para injeção de dependência

O C# 12 permite declarar os parâmetros do construtor ao lado do nome da classe, e usá-los direto no corpo. Isso apaga o trio que toda classe injetada repetia: o campo privado, o parâmetro do construtor e a linha que copia um no outro. Com o construtor primário, OrderService(IOrderRepository repository, INotifier notifier) já deixa repository e notifier disponíveis em qualquer método.

❌ Ruim: o construtor tradicional repete cada dependência três vezes
public class OrderService
{
    private readonly IOrderRepository _repository;
    private readonly INotifier _notifier;

    public OrderService(IOrderRepository repository, INotifier notifier)
    {
        _repository = repository;
        _notifier = notifier;
    }

    public async Task<Result<Order>> SaveOrderAsync(OrderRequest request, CancellationToken ct)
    {
        var order = Order.From(request);

        await _repository.SaveAsync(order, ct);
        await _notifier.SendAsync(order, ct);

        return Result<Order>.Success(order);
    }
}
✅ Bom: primary constructor, DI sem cerimônia
public class OrderService(IOrderRepository repository, INotifier notifier)
{
    public async Task<Result<Order>> SaveOrderAsync(OrderRequest request, CancellationToken ct)
    {
        var order = Order.From(request);

        await repository.SaveAsync(order, ct);
        await notifier.SendAsync(order, ct);

        return Result<Order>.Success(order);
    }
}

Uma linha em branco entre um passo e o próximo

As linhas que pertencem ao mesmo passo ficam juntas, sem linha em branco entre elas. Entre um passo e o seguinte entra exatamente uma linha em branco. Duas linhas em branco seguidas não separam nada a mais, só espalham o método pela tela e obrigam a rolar. O corte visual conta ao leitor onde termina uma ideia e começa a outra.

❌ Ruim: sem separação entre passos ou separação excessiva
public async Task<Result<Invoice>> ProcessOrderAsync(OrderRequest request, CancellationToken ct)
{
    var validationResult = ValidateRequest(request);
    if (validationResult.IsFailure)
        return Result<Invoice>.Fail(validationResult.Error!.Message, validationResult.Error.Code);
    var product = await _products.FindByIdAsync(request.ProductId, ct); // sem separação do bloco anterior
    if (product is null)
        return Result<Invoice>.Fail("Product not found.", "NOT_FOUND");
    var order = await SaveOrderAsync(request, product, ct);


    await NotifyOrderCreatedAsync(order, ct); // duas linhas em branco: ruído
    var invoice = BuildInvoice(order);

    return Result<Invoice>.Success(invoice);
}
✅ Bom: um grupo por passo, separados por uma linha em branco
public async Task<Result<Invoice>> ProcessOrderAsync(OrderRequest request, CancellationToken ct)
{
    var validationResult = ValidateRequest(request);
    if (validationResult.IsFailure)
        return Result<Invoice>.Fail(validationResult.Error!.Message, validationResult.Error.Code);

    var product = await _products.FindByIdAsync(request.ProductId, ct);

    if (product is null)
        return Result<Invoice>.Fail("Product not found.", "NOT_FOUND");

    var order = await SaveOrderAsync(request, product, ct);
    await NotifyOrderCreatedAsync(order, ct);

    var invoice = BuildInvoice(order);

    return Result<Invoice>.Success(invoice);
}

DoDocs v3.7.0 · Desenvolvido por @thiagocajadev · Baseado no trabalho de pmndrs/docs · Poimandres.