Source d'événement, un événement, l'état de deux agrégats a changé

10

J'essaie d'apprendre des façons de DDD et des sujets connexes. Je suis venu avec une idée de contexte borné simple pour mettre en œuvre la «banque»: il y a des comptes, l'argent peut être déposé, retiré et transféré entre eux. Il est également important de conserver l'historique des modifications.

J'ai identifié une entité de compte et cette source d'événements serait utile pour garder une trace de ses modifications. D'autres entités ou objets de valeur ne sont pas pertinents pour le problème, donc je ne les mentionnerai pas.

Lorsque l'on considère les dépôts et les retraits - c'est relativement simple, car il n'y a qu'un seul agrégat modifié.

Lors du transfert, c'est différent - deux agrégats doivent être modifiés par un événement MoneyTransferred . DDD déconseille de modifier plusieurs agrégats en une seule transaction. D'autre part, la règle du sourcing d'événements consiste à appliquer des événements aux entités et à modifier l'état en fonction de celles-ci. Si l'événement pouvait être stocké simplement dans la base de données, il n'y aurait aucun problème. Mais pour empêcher la modification simultanée des entités issues d'événements, nous devons implémenter quelque chose de versionner le flux d'événements de chaque agrégat (pour conserver leurs limites de transaction). Avec le versionnage vient un autre problème - je ne peux pas utiliser de structures simples pour stocker des événements et les relire pour les appliquer à l'agrégation.

Ma question est la suivante: comment puis-je réunir ces trois principes: "un agrégat une transaction", "événement-> changement d'agrégat" et "prévention des modifications simultanées"?

cocsackie
la source

Réponses:

7

Lors du transfert, c'est différent - deux agrégats doivent être modifiés par un événement MoneyTransferred.

Le transfert d'argent est un acte distinct de la mise à jour des registres.

MoneyTransferred
AccountCredited
AccountDebited

L'exercice qui a finalement rompu cela me rendait compte que AccountOverdrawnc'était un événement, il décrit l'état du compte sans tenir compte des autres participants à cet échange, donc il doit y avoir une commande exécutée sur un compte qui le produit.

Vous ne pouvez pas raisonnablement dériver un état comme à AccountOverdrawnpartir du modèle de lecture, car vous ne pouvez pas savoir si vous avez déjà vu tous les événements - seul l'agrégat lui-même a une vue complète de l'historique à un moment donné.

La réponse, bien sûr, est là dans le langage omniprésent - les comptes sont crédités ou débités pour refléter les obligations de la banque envers ses clients.

D'accord, mais cela signifie que je devrais également utiliser les événements AccountCredited et AccountDebited pour les dépôts et les retraits, donc je n'enregistre pas seulement la cause du changement, mais le changement provoqué par une autre action. Si je voudrais inverser l'action, je n'ai pas pu, car tous les événements ne sont pas enregistrés.

Je ne suis pas tout à fait certain de ce qui suit, car vous avez (pour des cas comme celui-ci) un identifiant de corrélation naturelle, qui est l'identifiant de transaction lui-même.

Deuxième chose - cela signifie que je dois utiliser quelque chose comme la saga.

Orthographe légèrement différente: vous avez besoin de quelque chose comme un être humain envoyant les bonnes commandes .

Il y a au moins deux façons de procéder. L'une consisterait à avoir un abonné à l'écoute MoneyTransferredet à envoyer les deux commandes aux registres.

Une autre alternative serait de suivre le traitement de la transaction comme un agrégat distinct - pensez-y comme une liste de contrôle de tout ce qui doit être fait depuis qu'une transaction a eu lieu. Un MoneyTransferredgestionnaire d'événements envoie donc ProcessTransaction, qui planifie le travail à effectuer et vérifie quel travail a été effectué.

VoiceOfUnreason
la source
Très bien, mais cela signifie que je devrais également utiliser les événements AccountCredited et AccountDebited pour les dépôts et les retraits, donc je n'enregistre pas seulement la cause du changement, mais le changement provoqué par une autre action. Si je voudrais inverser l'action, je n'ai pas pu, car tous les événements ne sont pas enregistrés. Comment puis-je faire cela (causalité des événements)? Deuxième chose - cela signifie que je dois utiliser quelque chose comme la saga. Comment devrait-on alors modéliser un transfert? Au moment où j'ai la méthode de transfert en compte. Lorsqu'il est appelé, il publie l'événement MoneyTransferred . Je ne sais pas ce qui devrait commencer quelque chose comme une saga.
cocsackie
Est - ce pas -> AccountCredited et AccoundDebited puis MoneyTransferred ? La première solution met à jour les deux agrégats en une seule transaction (aucune garantie de cohérence d' aucune sorte)? Il n'y a pas non plus d'agrégat qui pourrait publier MoneyTransferred -> pas de corrélation. La deuxième solution semble être meilleure - ProcessTransaction peut publier MoneyTransferred et pour éviter plusieurs modifications agrégées en une seule transaction, je peux publier des événements à partir du compte après avoir validé la transaction. Désolé d'être tatillon. Il est difficile à comprendre pour les débutants - ne peut pas utiliser un seul modèle sans autre.
cocsackie
1

Un détail important pour comprendre les comptes basés sur les transactions: l' balanceattribut de accountest en fait une instance de dénormalisation. C'est là pour plus de commodité. En réalité, le solde d'un compte est la somme de ses transactions, et vous n'avez pas vraiment besoin du compte lui-même pour avoir un solde.

Gardant cela à l'esprit, l'acte de transférer un argent ne devrait pas être de mettre à jour accountmais d'insérer transaction.

Cela étant dit, il existe une autre règle importante: l'acte d'ajouter un transactiondevrait être atomique avec une mise à jour du (champ d'équilibre dénormalisé de) account.

Maintenant, si je comprends le concept DDD d'agrégats, ce qui suit semble pertinent:

L'agrégat est une frontière logique pour les choses qui peuvent changer dans une transaction commerciale d'un contexte donné. Un agrégat peut être représenté par une seule classe ou par une multitude de classes. Si plusieurs classes constituent un agrégat, alors l'une d'elles est la classe ou entité dite racine. Tout accès à l'agrégat depuis l'extérieur doit se faire via la classe racine.

Donc, en termes de conception DDD, je suggère:

  1. Il y a un agrégat pour représenter le transfert

  2. L'agrégat est composé des objets suivants: le transfert (l'objet racine); l'objet racine est lié à deux listes de transactions (une pour chaque compte); et chaque liste de transactions est liée à un compte.

  3. Tout accès au transfert doit être médité par l'objet racine (le transfer).

Si vous essayez d'implémenter la prise en charge du transfert asynchrone, votre code principal devrait simplement s'inquiéter de la création du transfert, dans un état "en attente". Vous pouvez avoir un autre thread ou un travail qui déplace réellement l'argent (l'insertion dans l'historique des transactions, et donc la mise à jour des soldes) et définit le transfert sur "publié".

Si vous cherchez à implémenter une transaction de transfert bloquante en temps réel, la logique métier doit créer un transferet cet objet coordonnerait les autres activités en temps réel.

En termes de prévention des problèmes de concurrence, le premier ordre du jour devrait être d'insérer la transaction de débit dans la liste des transactions pour le compte source (mise à jour du solde, bien sûr). Cela devrait être effectué de manière atomique au niveau de la base de données (via une procédure stockée). Une fois le débit effectué, le reste du transfert devrait pouvoir réussir indépendamment des problèmes de concurrence, car aucune règle commerciale ne devrait empêcher un crédit sur le compte cible.

(Dans le monde réel, les comptes bancaires ont le concept d'un message mémo qui prend en charge un concept de validation en deux phases paresseux. La création du message mémo est légère et facile, et elle peut également être annulée sans problème. Conversion du La publication d'un mémo dans un message dur est le moment où l'argent se déplace réellement - cela ne peut pas être annulé - et représente la deuxième phase de la validation en deux phases, qui ne se produit qu'après que toutes les règles de validation ont été vérifiées).

John Wu
la source
0

Je suis également actuellement en phase d'apprentissage. Du point de vue de la mise en œuvre, voici comment je pense que vous allez effectuer cette action.

Dispatch TransferMoneyCommand qui déclenche les événements suivants [MoneyTransferEvent, AccountDebitedEvent]

Notez avant qu'il ne déclenche ces événements, une validation de commande superficielle et une validation de logique de domaine devront être effectuées, c'est-à-dire que le compte a-t-il un solde suffisant?

Conservez les événements (avec versioning) pour vous assurer qu'il n'y a pas de problèmes de cohérence. Notez qu'il pourrait y avoir une autre commande simultanée (comme retirer tout l'argent) qui a réussi et enregistré des événements avant celui-ci, donc l'état actuel de l'agrégat peut être obsolète et donc les événements sont déclenchés sur l'ancien état et sont incorrects. Si la sauvegarde des événements échoue, vous devrez réessayer la commande depuis le début.

Une fois les événements enregistrés avec succès dans la base de données, vous pouvez publier les deux événements qui ont été déclenchés.

AccountDebitedEvent supprimera l'argent du compte du payeur (met à jour l'état global et tous les modèles de vue / projection associés)

MoneyTransferEvent démarre Saga / Process Manager.

Le travail du responsable de la saga / du processus sera d'essayer de créditer le compte du bénéficiaire, s'il échoue, il devra créditer le solde au payeur.

Le responsable de la saga / processus publiera un CreditAccountCommand qui est appliqué au compte du bénéficiaire et en cas de succès, AccountCreditedEvent sera généré.

Du point de vue de la recherche d'événements, si vous souhaitez annuler cette action, tous les événements de cette transaction auront l'ID de corrélation / causalité comme TransferMoneyCommand d'origine que vous pouvez utiliser pour déclencher des événements pour les opérations d'annulation / d'annulation.

N'hésitez pas à suggérer des problèmes ou des améliorations potentielles sur ce qui précède.

Shayan C
la source