Seleccionar página

Ayer estuvimos analizando una HU en busca de la solución al problema. Llevamos tiempo usando DDD (Domain-Driven-Design), en algunos casos con más acierto que en otros, pero tras algún tiempo haciéndolo hemos ido «afinando el olfato». La realidad es que DDD aporta reglas y mecanismos para modelar tu negocio y garantizar su consistencia, y sólo son eso, reglas, las decisiones finales sobre tu arquitectura son decisión tuya. Exponemos este caso como ejemplo de lo comentado, nos salieron tres posibles soluciones que vamos a detallar.

El problema

Situados en el espacio del problema lo que se nos dice es que cada vez que se genere o modifique una oferta de compra a un cliente hay que entregársela, bien por email, bien impresa. Eso además implica que debemos guardar registro del documento y los datos asociados a la entrega, cosas como la fecha, el canal o el usuario que la ha efectuado. Esto implica que cada oferta va a tener una o varias entregas en función de si ha ido siendo modificada o no. Por otro lado cada oferta debe reflejar la fecha de la última entrega realizada así como el usuario que lo ha hecho y el canal utilizado.

La solución

Partimos de una situación en la que ya existe un agregado llamado Offer que contiene los datos de la oferta y los vehículos que contiene. Este agregado se persiste en una tabla de ofertas y otra de vehículos asociados a la misma.
Por razones en las que no nos interesa profundizar aquí, decidimos que para gestionar el histórico de envíos, además de tener una tabla con la lista de envíos, queremos añadir a la tabla de ofertas una columna con la fecha de la última entrega y otras con el usuario y el canal.
Basándonos en DDD, tendremos una acción SendOfferByEmail (limitemos la solución a resolver solo el caso de envío por email), esta acción será la encargada de crear el documento, enviarlo por email y registrarlo en el sistema.

Primera aproximación

Para la primera aproximación vamos a utilizar únicamente el agregado Offer. Incorporaremos una lista de la entidad OfferDelivery a nuestro agregado, de manera que será éste quien se encargue de las reglas de negocio aplicables tanto a la creación de una entrega como a la actualización de la última.
En la acción SendOfferByEmail:
[code language=»csharp»]
public async Task<OfferDocument> Execute(OfferDocumentRequest request) {
var document = await offerDocumentGenerator.GenerateOfferDelivery(request.OfferCode, request.PathToSaveDocument);
var offer = await offerRepo.Get(request.OfferCode);
offer.CreateDelivery(document.DocumentPath, DeliveryChannel.Email, UtcDate.Now, request.UserId);
offerEmailSender.SendDocument(document);
//Este update persiste tanto los cambios en la oferta como la nueva entrega
offerRepo.Update(offer);
}
[/code]
En en el agregado Offer:
[code language=»csharp»]
public void CreateDelivery(string documentPath, DeliveryChannel channel, DateTime date, string userId) {
//Comprueba las reglas de negocio de la creación de entrega relacionadas con la oferta
IfCannotCreateDeliveryThrowException();
//Crea la entrega. El constructor valida que el objeto quede consistente.
var offerDelivery = new OfferDelivery(this.Code, documentPath, channel, date, userId)
deliveries.add(offerDelivery);
//Actualiza los datos de la última entrega en la oferta
LastDeliveryUserId = offerDelivery.UserId;
LastDeliveryChannel = offerDelivery.Channel;
LastDeliveryDate = offerDelivery.Date;
}
[/code]
En el repositorio OfferRepository:
[code language=»csharp»]
public void Update(Offer offer){
transaction.Execute(() => {
UpdateOffer(offer);
UpdateVehicles(offer.vehicles);
UpdateDeliveries(offer.deliveries);
});
}
[/code]

Segunda aproximación

La segunda aproximación se basa en el agregado OfferDelivery, que además de las reglas de negocio de la entrega se va a encargar de mantener la cabecera de la oferta actualizada.
En la acción SendOfferByEmail:
[code language=»csharp»]
public async Task<OfferDocument> Execute(OfferDocumentRequest request) {
var document = await offerDocumentGenerator.GenerateOfferDelivery(request.OfferCode, request.PathToSaveDocument);
var offer = await offerRepo.Get(request.OfferCode);
var offerDelivery = new OfferDelivery(offer.Code, document.DocumentPath, DeliveryChannel.Email, UtcDate.Now, request.UserId);
offerEmailSender(document);
//Este insert persiste tanto la nueva oferta como la actualización de la última entrega en la oferta
offerDeliveryRepo.Insert(offerDelivery);
}
[/code]
En el agregado OfferDelivery:
[code language=»csharp»]
public OfferDelivery(string offerCode, string path, DeliveryChannel channel, DateTime date, string userId){
OfferCode = offer.Code;
Path = path;
Channel = channel;
Date = date;
UserId = userId;
IfDeliveryIsNotValidThrowException();
} [/code]
En el repositorio OfferDeliveryRepository:
[code language=»csharp»]
public void Insert(OfferDelivery offerDelivery){
transaction.Execute(() => {
InsertDelivery(offerDelivery);
UpdateOffer(offerDelivery.OfferCode, offerDelivery.Channel, offerDelivery.Date, offerDelivery.UserId);
});
}
[/code]
Con esta solución hay que tener cuidado porque la actualización de la última entrega en la oferta se hace en el repo. Esto podría llevar a inconsistencias si existiera lógica de negocio asociada a la cabecera de la oferta, puesto que se actualizarían datos en la misma sin pasar por su agregado. En este caso, OfferDelivery como agregado debería contener un Offer al que solicitarle que actualizara la última entrega.
En el agregado OfferDelivery:
[code language=»csharp»]
public OfferDelivery(Offer offer, string path, DeliveryChannel channel, DateTime date, string userId){
OfferCode = offer.Code;
Path = path;
Channel = channel;
Date = date;
UserId = userId;
IfDeliveryIsNotValidThrowException();
//offer se encarga de las reglas para modificar el último envío en la oferta
offer.ChangeLastDelivery(Date, Channel, UserId);
} [/code]
En en el agregado Offer:
[code language=»csharp»]
public void ChangeLastDelivery(DeliveryChannel channel, DateTime date, string userId) {
//Comprueba las reglas de negocio de la creación de entrega relacionadas con la oferta
IfCannotCreateDeliveryThrowException();
//Actualiza los datos de la última entrega en la oferta
LastDeliveryUserId = userId;
LastDeliveryChannel = channel;
LastDeliveryDate = date;
}
[/code]
En el repositorio OfferDeliveryRepository:
[code language=»csharp»]
public void Insert(OfferDelivery offerDelivery){
transaction.Execute(() =&amp;amp;amp;gt; {
InsertDelivery(offerDelivery);
UpdateOffer(offerDelivery.Offer);
});
}
[/code]

Tercera aproximación

Una tercera solución sería gestionar la entrega mediante dos agregados, de manera que Offer se encargue de actualizar los datos de la última oferta y OfferDelivery de la creación de la entrega. En este caso la consistencia se garantiza a nivel de acción, que delega la gestión de la transaccionalidad en un transactionScope que recibe como dependencia.
En la acción SendOfferByEmail:
[code language=»csharp»]
public async Task<OfferDocument> Execute(OfferDocumentRequest request) {
var document = await offerDocumentGenerator.GenerateOfferDelivery(request.OfferCode, request.PathToSaveDocument);
var offer = await offferRepo.Get(request.OfferCode);
var offerDelivery = new OfferDelivery(offer.Code, document.DocumentPath, DeliveryChannel.Email, UtcDate.Now, request.UserId);
offer.ChangeLastDelivery(offerDelivery.User, offerDelivery.Channel, offerDelivery.Date);
offerEmailSender.SendDocument(document);
transaction.Execute(() => {
offerDeliveryRepo.Insert(offerDelivery);
offerRepository.Update(offer);
});
return document;
}
[/code]
En el agregado OfferDelivery:
[code language=»csharp»]
public OfferDelivery(string offerCode, string path, DeliveryChannel channel, DateTime date){
OfferCode = offerCode;
Path = path;
Channel = channel;
Date = date;
IfDeliveryIsNotValidThrowException();
}
[/code]
En el agregado Offer:
[code language=»csharp»]
public void ChangeLastDelivery(string userId, DeliveryChannel channel, DateTime date) {
//Comprueba las reglas de negocio de la creación de entrega relacionadas con la oferta
IfCannotChangeLastDeliveryThrowException();
LastDeliveryUserId = userId;
LastDeliveryChannel = channel;
LastDeliveryDate = date;
}
[/code]
La conclusión a la que queremos llegar es que DDD no es una receta mágica que te libra de tomar decisiones estructurales, no te dice cómo debes convertir tu dominio y sub-dominios en bounded context o cómo dividir éstos en agregados, entidades o servicios. Tampoco quién debe encargarse de implementar cada regla de negocio o si debes elegir consistencia eventual o transacciones en acciones o repositorios.
En mi opinión lo que sí te da, y es mucho,  es una serie de reglas, patrones y elementos que sirven para tomar esas decisiones, herramientas que debes adaptar al contexto del problema concreto que tratas de solucionar, de manera que lo que siempre se garantice sea la consistencia e independencia de tu dominio.