Domain-Driven Design em C#: como não se afogar na complexidade
\n\nQuando seu projeto C# supera o estágio de "aplicação CRUD simples", chega o momento da verdade. A lógica de negócio começa a se embaralhar, os métodos dos serviços crescem para centenas de linhas, e a palavra "refatoração" causa tremores nervosos. Familiar? Bem-vindo ao mundo onde, sem Domain-Driven Design (DDD), você corre o risco de transformar o código em um pântano.
\n\nDDD não é sobre "mais um framework". É uma filosofia que coloca o domínio de negócio no centro do universo. Hoje vamos analisar como aplicar Domain-Driven Design em projetos C# para que o código se torne não apenas funcional, mas compreensível e flexível. Sem enrolação — apenas exemplos reais e dicas práticas.
\n\nPor que o DDD salva projetos C#?
\n\nImagine que você está escrevendo um sistema para gerenciar pedidos em uma loja online. Se você simplesmente colocar toda a lógica nos controladores ASP.NET, depois de seis meses não conseguirá explicar por que o método CreateOrder() chama CalculateShipping(), que, por sua vez, acessa o banco de dados para taxas de câmbio. O DDD oferece uma separação clara: existe uma camada de domínio, onde vive a lógica de negócio, e uma infraestrutura, que cuida dos detalhes técnicos.
A ideia central do DDD é a Ubiquitous Language (linguagem ubíqua). Você e os especialistas de negócio falam a mesma língua. No código, isso se expressa em termos como: "Order", "LineItem", "DiscountPolicy", e não "OrderDto", "OrderService" e "Helper". Quanto mais próximo o código estiver da linguagem do negócio, mais fácil será mantê-lo.
\n\nDesign estratégico e tático
\n\nO DDD se divide em duas partes: a estratégica (como dividir o sistema em módulos) e a tática (como escrever o código dentro do módulo). Em C#, o design tático nos fornece quatro blocos de construção poderosos:
\n\n- \n
- Entity — objeto com um identificador único (por exemplo,
OrderId). \n - Value Object — objeto que é definido por seus atributos (por exemplo,
MoneyouAddress). \n - Aggregate Root — entidade raiz que gerencia um grupo de objetos. \n
- Repository — abstração de acesso a dados. \n
Vamos ver como isso se parece no código.
\n\nExemplo: projetando um pedido (Order)
\n\nVamos começar com algo simples: temos um pedido que consiste em itens. O pedido pode estar no status "Novo", "Pago" ou "Cancelado".
\n\n1. Value Object: Money
\n\nDinheiro é um Value Object clássico. Dois objetos Money são iguais se tiverem o mesmo valor e moeda. Eles são imutáveis.
public sealed record Money(decimal Amount, string Currency)\n{\n public static Money operator +(Money left, Money right)\n {\n if (left.Currency != right.Currency)\n throw new InvalidOperationException("Currency mismatch");\n return new Money(left.Amount + right.Amount, left.Currency);\n }\n}\n\n\nObserve o record — em C# 9+ esta é a maneira ideal de criar um Value Object. Ele fornece automaticamente Equals, GetHashCode e imutabilidade.
2. Entity: Order e LineItem
\n\nAs entidades têm um identificador. Vamos criar uma classe base para Entity:
\n\npublic abstract class Entity<TId>\n{\n public TId Id { get; protected set; }\n\n protected Entity(TId id) => Id = id;\n\n public override bool Equals(object? obj) =>\n obj is Entity<TId> other && Id!.Equals(other.Id);\n\n public override int GetHashCode() => Id!.GetHashCode();\n}\n\n\nAgora o próprio Order e LineItem:
public class Order : Entity<Guid>\n{\n private readonly List<LineItem> _items = new();\n public IReadOnlyList<LineItem> Items => _items.AsReadOnly();\n public OrderStatus Status { get; private set; }\n public Money Total { get; privat