Llevo unos cuantos posts dedicados a la creación de modelos de dominio más ricos en lo que he ido explicando cómo convertir los tipos enumerados en clases, cómo pasar lógica de servicios a entidades y cómo utilizar eventos de dominio para reducir el acoplamiento entre componentes de nuestro modelo.
Al hablar de eventos de dominio vimos que era una buena forma de ir añadiendo comportamiento a nuestro modelo sin necesidad de tocar lo que ya existe, pero hay un factor muy importante al añadir esa funcionalidad que hemos de tener en cuenta: el aspecto transaccional.
Dónde se gestiona una transacción
En teoría el modelo de dominio debería ser independiente del tipo de persistencia que vayamos a emplear (aunque ya sabemos que la mayoría de ORMs imponen algunas restricciones sobre lo que podemos hacer y lo que no), pero lo que sí debemos tener presente al diseñarlo es la transaccionalidad de las operaciones que realizamos sobre él.
Las transacciones deberían controlarse siempre fuera del modelo de dominio puesto que las transacciones, conceptualmente, forman parte del caso de uso de la aplicación, no del modelo, pero el modelo debe estar diseñado para dar soporte a esas transacciones. Siguiendo con el ejemplo de la confirmación de un pedido que hemos usado en los últimos posts, tendríamos algo así:
public void ConfirmOrder(int orderId, int carrierId) { using (var tx = unitOfWork.BeginTransaction()) { var order = orderRepository.FindById(orderId); var carrier = carrierRepository.FindById(carrierId); order.Confirm(carrier, shippingCostCalculator); tx.Commit(); } }
El método ConfirmOrder
estaría en el punto de entrada a nuestra aplicación, que podría ser un controlador de ASP.NET MVC, de WebAPI, un bus de mensajes o algo similar. Las responsabilidades de esta capa de la aplicación deberían ceñirse a gestionar las transacciones, recuperar objetos de nuestro modelo y ponerlos a trabajar. En realidad, muchas veces incluso la gestión de transacciones se realiza de una forma «más general», a través de un layer supertype o incluso de forma declarativa mediante atributos.
La idea es que nuestro dominio realizará todo el trabajo y, si todo ha ido bien, la capa superior de la aplicación se encargará de hacer el commit de la transacción. En caso de que haya algún problema, se lanzará una excepción desde el dominio que provocará el rollback.
Excepciones en los manejadores de eventos de dominio
El problema es que cuando empezamos a utilizar eventos de dominio para integrar nuestro modelo con sistemas externos (enviar un email, imprimir un documento, etc.), estos sistemas externos puede que no siempre funcionen correctamente y generen excepciones, por lo que acabarían provocando que la transacción se abortase y deberemos pensar si eso es realmente lo que queremos que ocurra.
Por seguir con el ejemplo de los últimos posts, al confirmar el pedido habíamos decidido que era necesario reservar el stock correspondiente al pedido y enviar un email, pero ¿qué pasa si falla el envío de email? ¿Debemos abortar la transacción completa?
Dependiendo de cada caso, la gestión del evento deberá provocar el rollback o no. Por ejemplo en el caso del email, podríamos no abortar la transacción asumiendo que el usuario siempre podría acceder a una página web para ver si realmente se ha confirmado el pedido o no, pero en el caso de la reserva de stock, sí queremos que se ejecute en la misma transacción que la confirmación, porque si no podemos garantizar el stock, no queremos tomar el pedido (como he dicho en los otros posts, esto es un ejemplo y en la vida real estas decisiones son muy discutibles y, en cualquier caso, dependen del negocio, no de nosotros).
¿Cómo podemos solventar el problema? Una posible alternativa es gestionar la excepción dentro del dominio, en el punto en que se produce:
public class EmailSender : IDomainEventListener<OrderConfirmed> { public void Handle(OrderConfirmed @event) { try { // Enviar el email de confirmación al cliente... } catch (MailException ex) { // Hacer algo con la excepción } } }
El problema que tiene esta alternativa es que la excepción queda oculta dentro del propio listener, por lo que el resto del sistema no tiene forma de responder a ella, por ejemplo, avisando al usuario de que no se ha podido enviar el correo de confirmación para que revise la confirmación a través de la web. Sin embargo, si optamos por propagar la excepción ya hemos visto que estaríamos provocando que se abortase el proceso, cosa que tampoco queremos.
Otra opción podría ser capturar las excepciones en el servicio que está coordinando todo el proceso (el que está gestionando la transacción), algo parecido a esto:
public void ConfirmOrder(int orderId, int carrierId) { using (var tx = unitOfWork.BeginTransaction()) { var order = orderRepository.FindById(orderId); var carrier = carrierRepository.FindById(carrierId); try { order.Confirm(carrier, shippingCostCalculator); } catch (MailException ex) { // Hacer lo que sea necesario con la excepción } tx.Commit(); } }
Lo malo es que esto sigue sin resolver otro problema. Cuando un listener lanza una excepción, está rompiendo el flujo de ejecución de otros listeners que pudiésemos tener registrados por lo que la operación en el dominio quedaría a medio realizar.
Para solventar el problema, podemos aprovechar una herramienta que ya estamos utilizando: los propios eventos de dominio.
Podemos convertir la excepción en otro evento de dominio y, de esa forma, reaccionar ante el problema sin afectar a la transaccionalidad del proceso y sin detener el flujo de aplicación:
public class EmailSender : IDomainEventListener<OrderConfirmed> { public void Handle(OrderConfirmed @event) { try { // Enviar el email de confirmación al cliente... } catch (MailException ex) { DomainEvents.Raise(new MailFailed(@event.Order, ex)); } } }
Aplicando esta técnica, podremos enganchar otros listeners al evento MailFailed
y procesarlo como consideremos oportuno, por ejemplo registrándolo en un log, reintentando el envío o notificando al usuario por otros medios.
Resumen
Como ya dije en el post anterior sobre eventos de dominio, una de las principales ventajas de utilizarlos es que nos permite desacoplar unos componentes de otros dentro de nuestro modelo y añadir comportamiento sin modificar el código existente.
A la hora de diseñar un modelo de dominio en el que se va a hacer uso de eventos de dominio, hay que tener especial cuidado a la hora de pensar en qué operaciones queremos que se ejecuten de forma transaccional. Es fácil empezar a pensar en términos de listeners y olvidarnos de que forman parte de un proceso mayor, y aunque eso es útil para poder centrarse en cada parte de un proceso, también es un riesgo porque podemos pasar por alto las implicaciones que tiene cada listener y sus posibles errores sobre las transacciones del sistema.
Utilizando eventos de dominio para «propagar» las excepciones podemos manejar de una forma más homogénea nuestro dominio y aplicar las mismas técnicas e ideas que aplicamos para los casos normales.
Pese a todo, no hay que olvidar que todo tiene un precio y, en el caso de los eventos de dominio, la flexibilidad viene acompañada de una mayor complejidad y dificultad para seguir el código, por lo que es fundamental pensar en qué casos nos interesará aplicarlos y en qué casos será mejor recurrir a técnicas de programación más lineales.
Pues muchas gracias por la información Juan María, me viene ahora mismo de perlas para solucionar mi problema. genial :-)