DDD Táctico: Building Blocks del Dominio
En los capítulos anteriores, exploramos los fundamentos de Domain-Driven Design (DDD) y su enfoque estratégico para modelar dominios complejos. Ahora, nos adentramos en el DDD táctico, que proporciona los bloques de construcción para implementar el modelo del dominio en código. Como líder de KitsuneData Integral Solutions, he usado estos patrones para transformar procesos empresariales en sistemas robustos, y en este capítulo te guiaré para aplicarlos en .NET 9. Cubriremos entidades, objetos de valor, agregados, eventos de dominio, servicios de dominio, y un ejemplo práctico que modela un agregado “Pedido” en C#.
3.1 Entidades
Una entidad es un objeto con una identidad única que persiste a lo largo del tiempo, incluso si sus atributos cambian. En DDD, las entidades representan conceptos del dominio con un ciclo de vida definido, como un “Cliente” o un “Pedido”. La identidad suele implementarse con un identificador único (e.g., un Guid
).
Características de las entidades
- Identidad única: Un
Id
distingue cada instancia (e.g.,Cliente.Id
). - Mutabilidad controlada: Los atributos pueden cambiar, pero solo mediante métodos que reflejen reglas de negocio.
- Ciclo de vida: Las entidades tienen estados (e.g., un pedido puede estar “Creado” o “Enviado”).
Ejemplo en C#:
public class Cliente
{
public Guid Id { get; private set; }
public string Nombre { get; private set; }
public string Email { get; private set; }
private Cliente() { } // Para EF Core
public Cliente(string nombre, string email)
{
Id = Guid.NewGuid();
Nombre = nombre ?? throw new ArgumentNullException(nameof(nombre));
Email = email ?? throw new ArgumentNullException(nameof(email));
}
public void ActualizarEmail(string nuevoEmail)
{
if (string.IsNullOrEmpty(nuevoEmail))
throw new ArgumentException("El email no puede estar vacío.");
Email = nuevoEmail;
}
}
Este código define una entidad Cliente
con un Id
único y métodos que controlan cambios en sus atributos.
3.2 Objetos de valor
Un objeto de valor es un objeto sin identidad propia, definido por sus atributos. Son inmutables y se usan para describir aspectos del dominio que no cambian su identidad, como una “Dirección” o un “Monto”.
Características de los objetos de valor
- Inmutabilidad: No se modifican; se crean nuevas instancias para cambios.
- Igualdad por valor: Dos objetos de valor son iguales si sus atributos son idénticos.
- Simplicidad: Representan conceptos pequeños y cohesivos.
Ejemplo en C# (usando record
en .NET 9 para inmutabilidad):
public record Direccion(string Calle, string Ciudad, string CodigoPostal)
{
public Direccion Validar()
{
if (string.IsNullOrEmpty(Calle) || string.IsNullOrEmpty(Ciudad) || string.IsNullOrEmpty(CodigoPostal))
throw new ArgumentException("Todos los campos de la dirección son requeridos.");
return this;
}
}
La Direccion
es un objeto de valor inmutable, ideal para describir la ubicación de un cliente.
3.3 Agregados
Un agregado es un grupo de entidades y objetos de valor que se tratan como una unidad para garantizar la consistencia del dominio. Cada agregado tiene una raíz de agregado, una entidad que actúa como punto de entrada y controla las operaciones.
Reglas de los agregados
- Consistencia: Todas las operaciones en el agregado deben mantener sus reglas de negocio.
- Límite transaccional: Las operaciones externas solo acceden a la raíz del agregado.
- Tamaño reducido: Diseña agregados pequeños para minimizar la complejidad.
Ejemplo: Un agregado Pedido
incluye la entidad raíz Pedido
y objetos de valor LineaPedido
.
3.4 Eventos de dominio
Un evento de dominio representa un cambio relevante en el dominio, como “Pedido Confirmado”. Se usan para comunicar cambios entre contextos acotados o dentro de un mismo contexto.
Características
- Inmutables: Capturan un hecho que ya ocurrió.
- Descriptivos: Nombrados en pasado (e.g.,
PedidoConfirmado
). - Integración: Facilitan la comunicación asíncrona en microservicios.
Ejemplo en C#:
public record PedidoConfirmadoEvent(Guid PedidoId, Guid ClienteId, decimal Total, DateTime FechaConfirmacion);
3.5 Servicios de dominio
Un servicio de dominio encapsula lógica de negocio que no encaja naturalmente en una entidad o un objeto de valor. Por ejemplo, calcular descuentos o coordinar operaciones entre agregados.
Cuándo usar servicios
- Cuando la lógica involucra múltiples agregados.
- Cuando no pertenece a una entidad específica.
Ejemplo en C#:
public interface IDescuentoService
{
decimal CalcularDescuento(Guid clienteId, decimal total);
}
public class DescuentoService : IDescuentoService
{
public decimal CalcularDescuento(Guid clienteId, decimal total)
{
// Lógica de negocio: 10% de descuento para clientes VIP
bool esClienteVip = /* Consultar repositorio */;
return esClienteVip ? total * 0.9m : total;
}
}
3.6 Ejemplo
Apliquemos los conceptos a un agregado Pedido
en el contexto de un sistema de e-commerce (continuando el caso del Capítulo 1).
Escenario
- Contexto: Gestión de pedidos.
- Reglas de negocio:
- Un pedido tiene un cliente, líneas de pedido y un estado (“Creado”, “Confirmado”, “Enviado”).
- No se puede confirmar un pedido sin líneas.
- El total se calcula sumando los subtotales de las líneas.
- Componentes:
- Entidad raíz:
Pedido
. - Objeto de valor:
LineaPedido
. - Evento de dominio:
PedidoConfirmadoEvent
. - Servicio de dominio:
DescuentoService
.
- Entidad raíz:
Código
public class Pedido
{
public Guid Id { get; private set; }
public Guid ClienteId { get; private set; }
public List<LineaPedido> Lineas { get; private set; } = new();
public decimal Total { get; private set; }
public string Estado { get; private set; }
public Direccion DireccionEnvio { get; private set; }
private Pedido() { } // Para EF Core
public Pedido(Guid clienteId, Direccion direccionEnvio)
{
Id = Guid.NewGuid();
ClienteId = clienteId;
DireccionEnvio = direccionEnvio?.Validar() ?? throw new ArgumentNullException(nameof(direccionEnvio));
Estado = "Creado";
}
public void AgregarLinea(Guid productoId, int cantidad, decimal precio)
{
var linea = new LineaPedido(productoId, cantidad, precio);
Lineas.Add(linea);
Total += linea.Subtotal;
}
public void Confirmar(IDescuentoService descuentoService)
{
if (Lineas.Count == 0)
throw new InvalidOperationException("El pedido no tiene líneas.");
Total = descuentoService.CalcularDescuento(ClienteId, Total);
Estado = "Confirmado";
}
}
public record LineaPedido(Guid ProductoId, int Cantidad, decimal Precio)
{
public decimal Subtotal => Cantidad * Precio;
}
public record Direccion(string Calle, string Ciudad, string CodigoPostal)
{
public Direccion Validar()
{
if (string.IsNullOrEmpty(Calle) || string.IsNullOrEmpty(Ciudad) || string.IsNullOrEmpty(CodigoPostal))
throw new ArgumentException("Todos los campos de la dirección son requeridos.");
return this;
}
}
public record PedidoConfirmadoEvent(Guid PedidoId, Guid ClienteId, decimal Total, DateTime FechaConfirmacion);
public interface IDescuentoService
{
decimal CalcularDescuento(Guid clienteId, decimal total);
}
public class DescuentoService : IDescuentoService
{
public decimal CalcularDescuento(Guid clienteId, decimal total)
{
// Simulación: 10% de descuento para clientes VIP
bool esClienteVip = false; // Consultar repositorio en implementación real
return esClienteVip ? total * 0.9m : total;
}
}
Explicación del ejemplo
- Entidad raíz:
Pedido
gestiona el ciclo de vida y garantiza consistencia. - Objeto de valor:
LineaPedido
yDireccion
son inmutables y encapsulan lógica simple. - Evento de dominio:
PedidoConfirmadoEvent
captura la confirmación del pedido. - Servicio de dominio:
DescuentoService
maneja la lógica de descuentos. - Reglas de negocio: El método
Confirmar
valida que haya líneas y aplica descuentos.
En KitsuneData, hemos usado agregados como este para modelar procesos de negocio, asegurando que el código refleje las reglas del dominio. En el Capítulo 4, implementaremos la persistencia de este agregado con Entity Framework Core 9. Este capítulo ha presentado los bloques de construcción tácticos de DDD, listos para implementarse en .NET 9. En el próximo capítulo, exploraremos cómo integrar estos patrones con persistencia y ORMs.