Executando verificação de segurança...
5

Três Olhares sobre Arquitetura de Software: Fowler, Evans e Uncle Bob: 1.4 - Service Layer: orquestrando casos de uso

Capa

Nos últimos vinte anos, três nomes se tornaram referência inevitável sempre que falamos sobre arquitetura de software: Martin Fowler, Eric Evans e Robert C. Martin. Cada um deles trouxe uma perspectiva única, mas complementar, para o ofício de projetar sistemas duradouros. Fowler, com seus padrões de aplicação empresarial, nos ofereceu um catálogo de soluções pragmáticas para problemas recorrentes. Evans, com o Domain-Driven Design, nos mostrou como aproximar o software da linguagem do negócio e como dar forma ao que realmente importa em um sistema: o domínio. E Uncle Bob, com a Clean Architecture, reforçou os princípios e as camadas que protegem esse domínio da corrosão do tempo, dos frameworks e da infraestrutura.

Esta série de artigos nasce do desejo de cruzar esses três olhares. A cada padrão descrito por Fowler, vamos buscar o eco que ele encontra em Evans e em Martin. Em alguns momentos, haverá convergência; em outros, choque. Mas em todos, a discussão nos ajuda a entender não apenas o “como” programar, mas o “porquê” de certas escolhas arquiteturais.

Vamos usar exemplos em C#, trazer referências aos livros originais e sempre terminar com uma reflexão sobre quando e por que adotar (ou evitar) cada padrão. A ideia não é canonizar um estilo, mas oferecer um mapa de raciocínio para que você, como engenheiro de software, tenha mais clareza ao tomar suas decisões.


Índice da Série

Parte 1 – A lógica do domínio

Parte 2 – Persistência e Infraestrutura

Parte 3 – Apresentação e Integração

1.4 Service Layer: orquestrando casos de uso

Introdução

Quando no artigo anterior tratamos o Domain Model como o coração da aplicação, faltou falar da artéria que leva esse sangue aonde ele precisa chegar. É aqui que o Service Layer entra em cena. Martin Fowler descreveu esse padrão para dar uma “face” transacional à sua aplicação: em vez de UI, APIs ou jobs conversarem direto com entidades e agregados, elas pedem para um serviço executar um caso de uso com começo, meio e fim. Eric Evans reforça essa ideia ao separar com clareza o que é um Application Service, o coordenador do fluxo, do que é um Domain Service, um pedaço de regra de negócio que não coube naturalmente em uma entidade ou value object. E Robert C. Martin amarra tudo na Clean Architecture chamando essa camada de Use Cases, lembrando que ela deve ser independente de framework, banco e UI. Em outras palavras: o domínio diz o quê; o Service Layer diz o como; a infraestrutura diz o onde e o com o quê.

Service Layer como maestro dos fluxos de negócio

O pulo do gato está em não confundir orquestração com regra de negócio. O Service Layer existe para abrir e finalizar transações, coordenar repositórios, validar pré-condições mais “externas” (autorização, idempotência, consistência de parâmetros), publicar eventos de integração e, principalmente, contar a história de um caso de uso de ponta a ponta de forma testável. A lógica que muda o estado significativo do negócio — cálculo de preço, políticas de desconto, regras de estoque — precisa viver no domínio. Quando essa linha borra, o serviço cresce, fica inchado e vira um Transaction Script com roupa de objeto. Fowler alertou para esse cheiro há mais de vinte anos; Evans mostrou como evitá-lo com um modelo rico e serviços magros; e Uncle Bob acusaria a violação do SRP e a perda de independência da arquitetura. O antídoto é disciplina: Application Services pequenos, com nomes que soem como histórias reais (“RegistrarPedido”, “AprovarCrédito”, “CancelarReserva”), delegando o trabalho pesado às entidades, aos value objects, aos agregados e, quando fizer sentido, a Domain Services focados.

Para ficar concreto, imagine o caso de uso “Registrar um novo pedido”. O serviço de aplicação recebe o comando, garante a existência do cliente, cria o agregado Pedido e o persiste. Note que ele não “calcula” o pedido; ele pede para o agregado fazer isso. É essa separação que mantém o domínio expressivo e o fluxo claro e testável.

using System;
using System.Collections.Generic;
using System.Linq;

// Application Layer (Service Layer / Use Case)
public class RegisterOrderService
{
    private readonly IOrderRepository _orderRepository;
    private readonly ICustomerRepository _customerRepository;
    private readonly IUnitOfWork _uow;

    public RegisterOrderService(
        IOrderRepository orderRepository,
        ICustomerRepository customerRepository,
        IUnitOfWork uow)
    {
        _orderRepository = orderRepository;
        _customerRepository = customerRepository;
        _uow = uow;
    }

    public OrderId Execute(RegisterOrderCommand command)
    {
        if (command is null) throw new ArgumentNullException(nameof(command));
        if (command.Items is null || command.Items.Count == 0)
            throw new InvalidOperationException("Order must contain at least one item.");

        using var tx = _uow.Begin();

        var customer = _customerRepository.GetById(command.CustomerId);
        if (customer is null)
            throw new InvalidOperationException("Customer not found.");

        var order = Order.Create(customer.Id);

        foreach (var item in command.Items)
        {
            order.AddItem(item.ProductId, item.Quantity, item.UnitPrice);
        }

        order.Confirm();

        _orderRepository.Save(order);
        tx.Commit();

        return order.Id;
    }
}

// Command (input DTO)
public record RegisterOrderCommand(
    CustomerId CustomerId,
    List<RegisterOrderItemDto> Items);

public record RegisterOrderItemDto(
    ProductId ProductId,
    Quantity Quantity,
    Money UnitPrice);

// Domain (Aggregate Root)
public class Order
{
    private readonly List<OrderLine> _lines = new();

    public OrderId Id { get; }
    public CustomerId CustomerId { get; }
    public OrderStatus Status { get; private set; }
    public IReadOnlyList<OrderLine> Lines => _lines.AsReadOnly();

    private Order(OrderId id, CustomerId customerId)
    {
        Id = id;
        CustomerId = customerId;
        Status = OrderStatus.Draft;
    }

    public static Order Create(CustomerId customerId)
        => new(new OrderId(Guid.NewGuid()), customerId);

    public void AddItem(ProductId productId, Quantity quantity, Money unitPrice)
    {
        if (Status != OrderStatus.Draft)
            throw new InvalidOperationException("Cannot modify a confirmed order.");

        if (quantity.Value <= 0) throw new ArgumentOutOfRangeException(nameof(quantity));
        if (unitPrice.Amount <= 0) throw new ArgumentOutOfRangeException(nameof(unitPrice));

        var existingIndex = _lines.FindIndex(l => l.ProductId == productId);

        if (existingIndex < 0)
        {
            _lines.Add(new OrderLine(productId, quantity, unitPrice));
        }
        else
        {
            var existing = _lines[existingIndex];

            // Basic currency/price consistency check for the same product line.
            if (existing.UnitPrice.Currency != unitPrice.Currency)
                throw new InvalidOperationException("Cannot mix currencies for the same product line.");

            if (existing.UnitPrice.Amount != unitPrice.Amount)
                throw new InvalidOperationException("Cannot change unit price for an existing product line.");

            var increased = existing.Increase(quantity);
            _lines[existingIndex] = increased; // Replace VO instance (immutability preserved).
        }
    }

    public void Confirm()
    {
        if (!_lines.Any())
            throw new InvalidOperationException("Order must have at least one line.");

        Status = OrderStatus.Confirmed;
    }

    public Money Total()
    {
        if (!_lines.Any())
            throw new InvalidOperationException("Cannot compute total for an empty order.");

        var currency = _lines[0].UnitPrice.Currency;

        return _lines
            .Select(l => l.Total)
            .Aggregate(new Money(0m, currency), (acc, next) => acc.Add(next));
    }
}

public enum OrderStatus { Draft, Confirmed }

// Value Objects (VOs)
public readonly record struct OrderId(Guid Value);
public readonly record struct CustomerId(Guid Value);
public readonly record struct ProductId(Guid Value);

public readonly record struct Quantity(int Value)
{
    public Quantity Increase(int amount)
    {
        if (amount <= 0) throw new ArgumentOutOfRangeException(nameof(amount));
        checked { return new Quantity(Value + amount); }
    }
}

public readonly record struct Money(decimal Amount, string Currency)
{
    public Money Add(Money other)
    {
        if (Currency != other.Currency)
            throw new InvalidOperationException("Cannot add money with different currencies.");
        checked { return new Money(Amount + other.Amount, Currency); }
    }

    public Money Multiply(int factor)
    {
        if (factor < 0) throw new ArgumentOutOfRangeException(nameof(factor));
        checked { return new Money(Amount * factor, Currency); }
    }
}

// OrderLine is a Value Object (no identity, immutable, defined by its values).
public readonly record struct OrderLine(ProductId ProductId, Quantity Quantity, Money UnitPrice)
{
    public Money Total => UnitPrice.Multiply(Quantity.Value);

    public OrderLine Increase(Quantity additionalQuantity)
    {
        if (additionalQuantity.Value <= 0)
            throw new ArgumentOutOfRangeException(nameof(additionalQuantity));

        return this with { Quantity = Quantity.Increase(additionalQuantity.Value) };
    }
}

// Boundaries (Repositories / UoW)
public interface IOrderRepository
{
    void Save(Order order);
    Order? GetById(OrderId id);
}

public interface ICustomerRepository
{
    Customer? GetById(CustomerId id);
}

public interface IUnitOfWork : IDisposable
{
    ITransaction Begin();
}

public interface ITransaction : IDisposable
{
    void Commit();
}

// Just a placeholder for the example
public record Customer(CustomerId Id);

Repare como o serviço de aplicação cuida de fronteiras: valida parâmetros de entrada, abre a transação, busca o cliente, orquestra a criação do pedido e, por fim, persiste e confirma. Todo o miolo de negócio, invariantes, estados, cálculo, está encapsulado no agregado. É exatamente a relação que Evans defende entre Application Services e o modelo tático do DDD, enquanto Fowler chamaria isso de uma implementação limpa de Service Layer, e Uncle Bob veria um caso de uso independente de detalhes, pronto para ser testado com dublês de repositório e uma implementação in-memory de Unit of Work. Essa independência não é luxo: é o que permite evoluir tecnologia, dividir times por bounded contexts e manter o domínio respirando sem o cheiro da infraestrutura.

O que costuma dar errado é quando o Service Layer começa a “pensar pelo domínio”. A tentação aparece rápido: é fácil somar preços no serviço, validar estoque ali mesmo, chamar três APIs e consolidar o resultado. Funciona hoje, cobra caro amanhã. O serviço infla, fica difícil de ler, muda o tempo todo e os testes viram integrações frágeis. O caminho de volta é mover regra para onde ela pertence, dar nomes claros aos casos de uso, e aceitar que o serviço é o maestro, não a orquestra. É também nesse ponto que práticas de Refactoring (Fowler) e os hábitos de código limpo do Clean Code ajudam: funções pequenas, nomes que revelam intenção, eliminação de repetição e limites de responsabilidade bem desenhados. O benefício aparece na primeira refatoração sem medo e no primeiro teste que quebra pelo motivo certo.

Conclusão

No fim, o Service Layer não rouba o protagonismo do domínio; ele o protege. Dá uma rota única e explícita para cada história de negócio, documenta a coreografia do que acontece por dentro e deixa claro o contrato com o mundo de fora. Mantendo-o magro e expressivo, você ganha casos de uso testáveis, um modelo de domínio vivo e uma arquitetura que grita o que o sistema faz, não qual framework ele usa. No próximo capítulo, vamos descer a rampa para a persistência e encarar o Active Record: simples e sedutor, mas com implicações sérias quando visto sob a ótica do DDD e da Clean Architecture, e é nessa fricção que moram as melhores decisões.

Carregando publicação patrocinada...
1
1