Domain-Driven Design en C#: cómo no ahogarse en la complejidad
\n\nCuando su proyecto en C# supera la etapa de "aplicación CRUD simple", llega el momento de la verdad. La lógica de negocio comienza a enredarse, los métodos de los servicios crecen hasta cientos de líneas, y la palabra "refactorización" provoca un temblor nervioso. ¿Le suena familiar? Bienvenido al mundo donde, sin Domain-Driven Design (DDD), corre el riesgo de convertir el código en un pantano.
\n\nDDD no es "otro framework más". Es una filosofía que coloca el dominio del negocio en el centro del universo. Hoy analizaremos cómo aplicar Domain-Driven Design en proyectos C# para que el código no solo funcione, sino que sea comprensible y flexible. Sin rodeos: solo ejemplos reales y consejos prácticos.
\n\n¿Por qué DDD salva proyectos en C#?
\n\nImagine que está escribiendo un sistema para gestionar pedidos en una tienda en línea. Si simplemente coloca toda la lógica en los controladores de ASP.NET, en seis meses no podrá explicar por qué el método CreateOrder() llama a CalculateShipping(), que a su vez accede a la base de datos para obtener tipos de cambio. DDD propone una separación clara: existe una capa de dominio, donde vive la lógica de negocio, y una infraestructura, que se encarga de los detalles técnicos.
La idea clave de DDD es el Ubiquitous Language (lenguaje ubicuo). Usted y los expertos del negocio hablan el mismo idioma. En el código, esto se expresa en términos como "Order", "LineItem", "DiscountPolicy", y no "OrderDto", "OrderService" o "Helper". Cuanto más se acerque el código al lenguaje del negocio, más fácil será mantenerlo.
\n\nDiseño estratégico y táctico
\n\nDDD se divide en dos partes: la estratégica (cómo dividir el sistema en módulos) y la táctica (cómo escribir código dentro del módulo). En C#, el diseño táctico nos proporciona cuatro bloques de construcción poderosos:
\n\n- \n
- Entity — objeto con un identificador único (por ejemplo,
OrderId). \n - Value Object — objeto que se define por sus atributos (por ejemplo,
MoneyoAddress). \n - Aggregate Root — entidad raíz que gestiona un grupo de objetos. \n
- Repository — abstracción de acceso a datos. \n
Veamos cómo se ve esto en el código.
\n\nEjemplo: diseñando un pedido (Order)
\n\nEmpecemos con algo simple: tenemos un pedido que consta de líneas de artículo. El pedido puede estar en estado "Nuevo", "Pagado" o "Cancelado".
\n\n1. Value Object: Money
\n\nEl dinero es un Value Object clásico. Dos objetos Money son iguales si tienen el mismo monto y moneda. Son inmutables.
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 el uso de record — en C# 9+ es la forma ideal de crear un Value Object. Proporciona automáticamente Equals, GetHashCode e inmutabilidad.
2. Entity: Order y LineItem
\n\nLas entidades tienen un identificador. Creemos una clase 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\nAhora, el propio Order y 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