¿Es seguro publicar un evento de dominio antes de conservar el agregado?

Resuelto DmitriBodiu asked hace 7 años • 4 respuestas

En muchos proyectos diferentes, he visto 2 enfoques diferentes para generar eventos de dominio.

  1. Generar evento de dominio directamente desde el agregado. Por ejemplo, imagina que tienes un agregado de clientes y aquí hay un método dentro de él:

    public virtual void ChangeEmail(string email)
    {
        if(this.Email != email)
        {
            this.Email = email;
            DomainEvents.Raise<CustomerChangedEmail>(new CustomerChangedEmail(email));
        }
    }
    

    Puedo ver 2 problemas con este enfoque. La primera es que el evento se genera independientemente de si el agregado persiste o no. Imagínese si desea enviar un correo electrónico a un cliente después de un registro exitoso. Se generará un evento "CustomerChangedEmail" y algún IEmailSender enviará el correo electrónico incluso si el agregado no se guardó. El segundo problema con la implementación actual es que cada evento debería ser inmutable. Entonces la pregunta es ¿cómo puedo inicializar su propiedad "OccuredOn"? ¡Solo dentro del agregado! Es lógico, ¿verdad? ¡Me obliga a pasar ISystemClock (abstracción de tiempo del sistema) a todos y cada uno de los métodos en conjunto! Queeee??? ¿No encuentra este diseño frágil y engorroso? Esto es lo que se nos ocurrirá:

    public virtual void ChangeEmail(string email, ISystemClock systemClock)
    {
        if(this.Email != email)
        {
            this.Email = email;
            DomainEvents.Raise<CustomerChangedEmail>(new CustomerChangedEmail(email, systemClock.DateTimeNow));
        }
    }
    
  2. El segundo enfoque es seguir lo que recomienda el patrón de abastecimiento de eventos. En todos y cada uno de los agregados, definimos una lista (Lista) de eventos no confirmados. ¡Preste atención a que UncommitedEvent no es un evento de dominio! Ni siquiera tiene la propiedad OccuredOn. Ahora, cuando se llama al método ChangeEmail en Customer Aggregate, no generamos nada. Simplemente guardamos el evento en la colección uncommitedEvents que existe en nuestro agregado. Como esto:

    public virtual void ChangeEmail(string email)
    {
        if(this.Email != email)
        {
            this.Email = email;
            UncommitedEvents.Add(new CustomerChangedEmail(email));
        }
    }
    

Entonces, ¿cuándo se genera el evento de dominio real? Esta responsabilidad se delega a la capa de persistencia. En ICustomerRepository tenemos acceso a ISystemClock, porque podemos inyectarlo fácilmente dentro del repositorio. Dentro del método Save() de ICustomerRepository debemos extraer todos los eventos no confirmados de Aggregate y para cada uno de ellos crear un DomainEvent. Luego configuramos la propiedad OccuredOn en el evento de dominio recién creado. Luego, EN UNA TRANSACCIÓN guardamos el agregado y publicamos TODOS los eventos del dominio. De esta manera estaremos seguros de que todos los eventos se plantearán en fronteras transnacionales con persistencia agregada.
¿Qué no me gusta de este enfoque? No quiero crear 2 tipos diferentes para el mismo evento, es decir, para el comportamiento de CustomerChangedEmail debería tener el tipo CustomerChangedEmailUncommited y CustomerChangedEmailDomainEvent. Sería bueno tener un solo tipo. ¡Por favor comparte tu experiencia con respecto a este tema!

DmitriBodiu avatar Apr 16 '17 17:04 DmitriBodiu
Aceptado

No soy partidario de ninguna de las dos técnicas que presentas :)

Hoy en día prefiero devolver un evento o un objeto de respuesta del dominio:

public CustomerChangedEmail ChangeEmail(string email)
{
    if(this.Email.Equals(email))
    {
        throw new DomainException("Cannot change e-mail since it is the same.");
    }

    return On(new CustomerChangedEmail { EMail = email});
}

public CustomerChangedEmail On(CustomerChangedEmail customerChangedEmail)
{
    // guard against a null instance
    this.EMail = customerChangedEmail.EMail;

    return customerChangedEmail;
}

De esta manera, no necesito realizar un seguimiento de mis eventos no comprometidos y no dependo de una clase de infraestructura global como DomainEvents. La capa de aplicación controla las transacciones y la persistencia de la misma manera que lo haría sin ES.

En cuanto a coordinar la publicación/guardado: normalmente ayuda otra capa de dirección indirecta. Debo mencionar que considero que los eventos ES son diferentes de los eventos del sistema. Los eventos del sistema son aquellos entre contextos acotados. Una infraestructura de mensajería dependería de eventos del sistema, ya que normalmente transmitirían más información que un evento de dominio.

Por lo general, al coordinar cosas como el envío de correos electrónicos, se utilizaría un administrador de procesos o alguna otra entidad para llevar el estado. Puede llevar esto consigo Customercon algunos DateEMailChangedSenty, si es nulo, es necesario enviarlo.

Los pasos son:

  • Comenzar transacción
  • Obtener flujo de eventos
  • Realice una llamada para cambiar el correo electrónico del cliente y agréguelo incluso al flujo de eventos
  • Se requiere registrar el envío de correo electrónico (DateEMailChangedSent vuelve a ser nulo)
  • Guardar secuencia de eventos (1)
  • Enviar un SendEMailChangedCommandmensaje (2)
  • Confirmar transacción (3)

Hay un par de formas de realizar la parte de envío de mensajes que pueden incluirlo en la misma transacción (no 2PC), pero ignoremos eso por ahora.

Suponiendo que anteriormente habíamos enviado un correo electrónico y nuestro DateEMailChangedSenttiene un valor antes de comenzar, podemos encontrarnos con las siguientes excepciones:

(1) Si no podemos guardar el flujo de eventos, entonces no hay problema ya que la excepción revertirá la transacción y el procesamiento ocurrirá nuevamente.
(2) Si no podemos enviar el mensaje debido a alguna falla en la mensajería, entonces no hay problema ya que la reversión restablecerá todo a antes de comenzar. (3) Bueno, hemos enviado nuestro mensaje, por lo que una excepción en la confirmación puede parecer un problema, pero recuerde que no podemos configurar la espalda DateEMailChangedSentpara nullindicar que necesitamos que se envíe un nuevo correo electrónico.

El controlador de mensajes para el SendEMailChangedCommandverificaría el DateEMailChangedSenty, si no, nullsimplemente regresaría, acusaría recibo del mensaje y desaparecería. Sin embargo, si es nulo, entonces enviaría el correo interactuando directamente con la puerta de enlace de correo electrónico o haciendo uso de algún punto final de servicio de infraestructura a través de mensajería (preferiría eso).

Bueno, esa es mi opinión de todos modos :)

Eben Roux avatar Apr 17 '2017 07:04 Eben Roux

He visto 2 enfoques diferentes para generar eventos de dominio.

Históricamente ha habido dos enfoques diferentes. Evans no incluyó eventos de dominio al describir los patrones tácticos del diseño impulsado por dominio; vinieron después .

En un enfoque, los eventos de dominio actúan como un mecanismo de coordinación dentro de una transacción . Udi Dahan escribió varias publicaciones describiendo este patrón y llegó a la conclusión:

Tenga en cuenta que el código anterior se ejecutará en el mismo hilo dentro de la misma transacción que el trabajo normal del dominio, por lo que debe evitar realizar actividades de bloqueo, como el uso de SMTP o servicios web.

abastecimiento de eventos, la alternativa común, es en realidad un animal muy diferente, en la medida en que los eventos se escriben en el libro de registro, en lugar de usarse simplemente para coordinar actividades en el modelo de escritura.

El segundo problema con la implementación actual es que cada evento debería ser inmutable. Entonces la pregunta es ¿cómo puedo inicializar su propiedad "OccuredOn"? ¡Solo dentro del agregado! Es lógico, ¿verdad? ¡Me obliga a pasar ISystemClock (abstracción de tiempo del sistema) a todos y cada uno de los métodos en conjunto!

Por supuesto: consulte los archivos del plan de John Carmack.

Si no considera el tiempo como un valor de entrada, piénselo hasta que lo haga: es un concepto importante.

En la práctica, en realidad hay dos conceptos de tiempo importantes a considerar. Si el tiempo es parte de su modelo de dominio, entonces es un insumo.

Si el tiempo son solo metadatos que estás tratando de preservar, entonces el agregado no necesariamente necesita saberlo: puedes adjuntar los metadatos al evento en otro lugar. Una respuesta, por ejemplo, sería utilizar una instancia de una fábrica para crear los eventos, siendo la propia fábrica responsable de adjuntar los metadatos (incluida la hora).

¿Cómo puede lograrse? Un ejemplo de código de muestra me ayudaría mucho.

El ejemplo más sencillo es pasar la fábrica como argumento del método.

public virtual void ChangeEmail(string email, EventFactory factory)
{
    if(this.Email != email)
    {
        this.Email = email;
        UncommitedEvents.Add(factory.createCustomerChangedEmail(email));
    }
}

Y el flujo en la capa de aplicación se parece a

  1. Crear metadatos a partir de la solicitud
  2. Crear la fábrica a partir de los metadatos.
  3. Pase la fábrica como argumento.

Luego, EN UNA TRANSACCIÓN guardamos el agregado y publicamos TODOS los eventos del dominio. De esta manera estaremos seguros de que todos los eventos se plantearán en fronteras transnacionales con persistencia agregada.

Como regla general, la mayoría de las personas intentan evitar el compromiso en dos fases siempre que sea posible.

En consecuencia, la publicación no suele ser parte de la transacción, sino que se realiza por separado. Vea la charla de Greg Young sobre Polyglot Data . El flujo principal es que los suscriptores extraen eventos del libro de registro. En ese diseño, el modelo push es una optimización de la latencia.

VoiceOfUnreason avatar Apr 16 '2017 14:04 VoiceOfUnreason

Tiendo a implementar eventos de dominio utilizando el segundo enfoque.

En lugar de recuperar manualmente y luego enviar todos los eventos en el repositorio de raíces agregadas, tengo una DomainEventDispatcherclase simple (capa de aplicación) que escucha varios eventos de persistencia en la aplicación. Cuando se agrega, actualiza o elimina una entidad, se determina si es un archivo AggregateRoot. Si es así, llama releaseEvents()y devuelve una colección de eventos de dominio que luego se envían mediante la aplicación EventBus.

No sé por qué te concentras tanto en la occurredOnpropiedad.

La capa de dominio solo se ocupa de la esencia de los eventos del dominio, como los ID de raíz agregados, los ID de entidad y los datos de objetos de valor.

En la capa de aplicación, puede tener un sobre de eventos que puede envolver cualquier evento de dominio serializado y al mismo tiempo brindarle algunos metadatos, como una identificación única (UUID/GUID), de qué raíz agregada proviene, la hora en que ocurrió, etc. persistió en una base de datos.

Estos metadatos son útiles en la capa de aplicación porque es posible que esté publicando estos eventos en otras aplicaciones utilizando un bus de mensajes/flujo de eventos a través de HTTP y permite que cada evento sea identificable de forma única.

Nuevamente, estos metadatos sobre el evento generalmente no tienen sentido en la capa de dominio, solo en la capa de aplicación. A la capa de dominio no le importan ni tiene ningún uso los ID de eventos o la hora en que ocurrieron, pero otras aplicaciones que consumen estos eventos sí. Es por eso que estos datos se adjuntan en la capa de aplicación.

daviomey avatar Apr 19 '2017 14:04 daviomey

La forma en que resolvería el problema del envío de correo electrónico es desvinculando la publicación del evento y el manejo del evento a través de una cola de mensajes. De esta manera cierras la transacción después de enviar el evento a la cola, y el envío de correo electrónico, u otros efectos que no pueden o no deben ser parte de la transacción DB original, ocurrirán poco después, en una transacción diferente. La forma más sencilla de hacerlo, por supuesto, es tener un controlador de eventos que publique eventos de dominio en la cola.

Si desea estar más seguro de que los eventos del dominio se publicarán en la cola cuando se confirme la transacción, puede guardar los eventos en una tabla OUTBOX que se confirmará con la transacción y luego hacer que un hilo lea desde la tabla y publicar en la cola de eventos

xpmatteo avatar May 22 '2019 20:05 xpmatteo