Est-il prudent de publier Domain Event avant de conserver l’agrégat?

Dans de nombreux projets différents, j’ai vu 2 approches différentes pour élever des événements de domaine.

  1. Relancez l’événement de domaine directement à partir de l’agrégat. Par exemple, imaginez que vous avez un agrégat client et qu’il contient une méthode:

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

    Je peux voir 2 problèmes avec cette approche. La première est que l’événement est déclenché, que l’agrégat soit persistant ou non. Imaginez si vous souhaitez envoyer un courrier électronique à un client après une inscription réussie. Un événement “CustomerChangedEmail” sera déclenché et certains IEmailSender enverront le courrier électronique même si l’ensemble n’a pas été enregistré. Le deuxième problème de la mise en œuvre actuelle est que chaque événement doit être immuable. La question est donc comment puis-je initialiser sa propriété “OccuredOn”? Seulement dans l’agrégat! C’est logique, non! Cela me force à passer ISystemClock (abstraction du temps système) à chaque méthode sur l’ensemble! Whaaat ??? Ne trouvez-vous pas cette conception fragile et encombrante? Voici ce que nous allons trouver:

     public virtual void ChangeEmail(ssortingng email, ISystemClock systemClock) { if(this.Email != email) { this.Email = email; DomainEvents.Raise(new CustomerChangedEmail(email, systemClock.DateTimeNow)); } } 
  2. La deuxième approche consiste à définir ce que recommande Event Pattern, le modèle d’approvisionnement. Sur chaque agrégat, nous définissons une liste (liste) d’événements non validés. S’il vous plaît payAttention que UncommitedEvent n’est pas un événement de domaine! Il n’a même pas la propriété OccuredOn. Désormais, lorsque la méthode ChangeEmail est appelée sur Customer Aggregate, nous ne soulevons rien. Nous venons de sauvegarder l’événement dans la collection uncommitedEvents qui existe sur notre agrégat. Comme ça:

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

Alors, quand l’événement de domaine réel est-il déclenché ??? Cette responsabilité est déléguée à la couche de persistance. Dans ICustomerRepository, nous avons access à ISystemClock, car nous pouvons facilement l’injecter à l’intérieur du référentiel. Dans la méthode Save () de ICustomerRepository, nous devons extraire tous les événements non engagés de Aggregate et créer pour chacun d’eux un événement DomainEvent. Ensuite, nous configurons la propriété OccuredOn sur un événement de domaine nouvellement créé. Ensuite, IN ONE TRANSACTION, nous sauvegardons l’agrégat et publions TOUS les événements de domaine. De cette façon, nous serons sûrs que tous les événements seront soulevés dans une frontière transnationale avec une persistance globale.
Qu’est-ce que je n’aime pas à propos de cette approche? Je ne souhaite pas créer 2 types différents pour le même événement, c’est-à-dire que le comportement CustomerChangedEmail doit être de type CustomerChangedEmailUncommited et CustomerChangedEmailDomainEvent. Ce serait bien d’avoir un seul type. S’il vous plaît partager votre expérience en ce qui concerne ce sujet!

Je ne suis partisan d’aucune des deux techniques que vous présentez 🙂

De nos jours, je suis favorable au retour d’un événement ou d’un object de réponse du domaine:

 public CustomerChangedEmail ChangeEmail(ssortingng 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 cette façon, je n’ai pas besoin de suivre mes événements non validés et je ne repose pas sur une classe d’infrastructure globale telle que DomainEvents . La couche application contrôle les transactions et la persistance de la même manière que sans ES.

En ce qui concerne la coordination de la publication / sauvegarde: une autre couche d’indirection aide généralement. Je dois mentionner que je considère les événements ES comme différents des événements système. Les événements système étant ceux entre des contextes liés. Une infrastructure de messagerie s’appuierait sur des événements système, ceux-ci transmettant généralement plus d’informations qu’un événement de domaine.

Habituellement, lorsqu’il s’agit de coordonner des choses telles que l’envoi de courriels, on fait appel à un gestionnaire de processus ou à une autre entité pour gérer l’état. Vous pouvez porter ceci sur votre Customer avec une certaine DateEMailChangedSent et si null, l’envoi est requirejs.

Les étapes sont:

  • Commencer la transaction
  • Obtenir un stream d’événements
  • Faire un appel pour changer le courrier électronique sur le client, ajoutant même au stream d’événements
  • enregistrement de l’envoi de courrier électronique requirejs (DateEMailChangedSent back to null)
  • Enregistrer le stream d’événements (1)
  • Envoyer un message SendEMailChangedCommand (2)
  • Transaction validée (3)

Il y a plusieurs façons de créer cette partie d’envoi de message qui peut l’ inclure dans la même transaction (pas de 2PC), mais ignorons cela pour l’instant.

En supposant que nous ayons précédemment envoyé un courrier électronique, notre DateEMailChangedSent a une valeur avant de commencer, nous risquons de rencontrer les exceptions suivantes:

(1) Si nous ne pouvons pas enregistrer le stream d’événements, le problème est résolu car l’exception annulera la transaction et le traitement se reproduira.
(2) Si nous ne pouvons pas envoyer le message en raison d’une défaillance de la messagerie, il n’y a pas de problème puisque la restauration rétablira tout avant le début. (3) Eh bien, nous avons envoyé notre message, une exception sur commit peut sembler un problème, mais rappelez-vous que nous ne pouvions pas DateEMailChangedSent notre DateEMailChangedSent sur null pour indiquer que nous demandons l’envoi d’un nouveau courrier électronique.

Le gestionnaire de messages pour SendEMailChangedCommand vérifiera le DateEMailChangedSent et si non null il renverra simplement, en accusant réception du message et il disparaîtra. Cependant, s’il est nul, le courrier sera alors soit en interaction avec la passerelle de messagerie, soit en utilisant un sharepoint terminaison de service d’infrastructure via la messagerie (je préférerais cela).

Eh bien, c’est mon sharepoint vue quand même 🙂

J’ai tendance à mettre en œuvre des événements de domaine en utilisant la deuxième approche.

Au lieu de récupérer manuellement puis de dissortingbuer tous les événements dans le référentiel de racines agrégé, j’ai une simple DomainEventDispatcher (couche d’application) qui écoute divers événements de persistance dans l’application. Lorsqu’une entité est ajoutée, mise à jour ou supprimée, elle détermine s’il s’agit d’une AggregateRoot . Si tel est le cas, il appelle releaseEvents() qui renvoie une collection d’événements de domaine qui sont ensuite dissortingbués à l’aide de l’application EventBus .

Je ne sais pas pourquoi vous vous concentrez autant sur la propriété

La couche de domaine ne concerne que la partie constitutive des événements de domaine, tels que les ID de racine agrégés, les ID d’entité et les données d’object de valeur.

Au niveau de la couche application, vous pouvez avoir une enveloppe d’événement capable d’envelopper tout événement de domaine sérialisé tout en lui donnant des métadonnées telles qu’un identifiant unique (UUID / GUID), sa racine globale, son heure, etc. persisté à une firebase database.

Ces métadonnées sont utiles dans la couche d’application, car vous pourriez publier ces événements dans d’autres applications à l’aide d’un bus de messages / d’un stream d’événements via HTTP et permettre à chaque événement d’être identifiable de manière unique.

Encore une fois, ces métadonnées sur l’événement n’ont généralement aucun sens dans la couche de domaine, mais uniquement dans la couche d’application. La couche de domaine ne se soucie pas ou n’a aucune utilité pour les ID d’événement ou l’heure à laquelle ils se sont produits, mais les autres applications qui consumnt ces événements l’utilisent. C’est pourquoi ces données sont attachées au niveau de la couche d’application.

J’ai vu 2 approches différentes d’élever des événements de domaine.

Historiquement, il y a eu deux approches différentes. Evans n’incluait pas les événements de domaine lorsqu’il décrivait les schémas tactiques de la conception pilotée par domaine; ils sont venus plus tard .

Dans une approche, les événements de domaine agissent comme un mécanisme de coordination au sein d’une transaction . Udi Dahan a écrit un certain nombre de posts décrivant ce modèle, pour arriver à la conclusion:

Sachez que le code ci-dessus sera exécuté sur le même thread au sein de la même transaction que le travail de domaine normal. Vous devez donc éviter toute opération de blocage, telle que l’utilisation de SMTP ou de services Web.

événement-sourcing , l’alternative commune, est en réalité un animal très différent, dans la mesure où les événements sont écrits dans le livre des enregistrements, plutôt que d’être simplement utilisés pour coordonner des activités dans le modèle d’écriture.

Le deuxième problème de la mise en œuvre actuelle est que chaque événement doit être immuable. La question est donc comment puis-je initialiser sa propriété “OccuredOn”? Seulement dans l’agrégat! C’est logique, non! Cela me force à passer ISystemClock (abstraction du temps système) à chaque méthode sur l’ensemble!

Bien sûr – voir les fichiers de plan de John Carmack

Si vous ne considérez pas le temps comme une valeur d’entrée, réfléchissez-y jusqu’à ce que vous le fassiez – c’est un concept important

En pratique, il faut tenir compte de deux concepts temporels importants. Si le temps fait partie de votre modèle de domaine, il s’agit d’une entrée.

Si vous essayez de préserver le temps que vous souhaitez préserver, alors l’agrégat n’a pas nécessairement besoin de le savoir – vous pouvez attacher les métadonnées à l’événement ailleurs. Une solution, par exemple, consisterait à utiliser une instance de fabrique pour créer les événements, l’usine elle-même étant responsable de l’attachement des métadonnées (y compris l’heure).

Comment peut-on y arriver? Un exemple d’échantillon de code m’aiderait beaucoup.

L’exemple le plus simple consiste à transmettre la fabrique en tant qu’argument à la méthode.

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

Et le stream dans la couche d’application ressemble à quelque chose comme

  1. Créer des métadonnées à partir de la demande
  2. Créer la fabrique à partir des métadonnées
  3. Passe l’usine comme un argument.

Ensuite, IN ONE TRANSACTION, nous sauvegardons l’agrégat et publions TOUS les événements de domaine. De cette façon, nous serons sûrs que tous les événements seront soulevés dans une frontière transnationale avec une persistance globale.

En règle générale, la plupart des gens essaient d’éviter autant que possible la validation en deux phases.

Par conséquent, publier ne fait généralement pas partie de la transaction, mais est tenu séparément. Voir le discours de Greg Young sur Polyglot Data . Le stream principal est que les abonnés extraient des événements du livre d’enregistrement. Dans cette conception, le modèle Push est une optimisation de la latence.