Comment exactement une commande CQRS doit-elle être validée et transformée en objet de domaine?

22

J'adapte le CQRS 1 du pauvre depuis un certain temps maintenant parce que j'aime sa flexibilité d'avoir des données granulaires dans un magasin de données, offrant de grandes possibilités d'analyse et augmentant ainsi la valeur commerciale et, si nécessaire, un autre pour les lectures contenant des données dénormalisées pour des performances accrues .

Mais malheureusement, depuis le début, je me bats avec le problème où je devrais placer la logique métier dans ce type d'architecture.

D'après ce que je comprends, une commande est un moyen de communiquer l'intention et n'a pas de liens avec un domaine en soi. Ce sont essentiellement des objets de transfert de données (stupides - si vous le souhaitez). Il s'agit de rendre les commandes facilement transférables entre différentes technologies. Il en va de même pour les événements en tant que réponses aux événements terminés avec succès.

Dans une application DDD typique, la logique métier réside au sein d'entités, d'objets de valeur, de racines agrégées, elles sont riches à la fois en données et en comportement. Mais une commande n'est pas un objet de domaine, elle ne doit donc pas se limiter aux représentations de données de domaine, car cela met trop de pression sur elles.

La vraie question est donc: où est exactement la logique?

J'ai découvert que j'ai tendance à faire face à cette lutte le plus souvent lorsque j'essaie de construire un agrégat assez compliqué qui établit des règles sur les combinaisons de ses valeurs. De plus, lors de la modélisation d'objets de domaine, j'aime suivre le paradigme de l' échec rapide , sachant qu'un objet atteint une méthode, il est dans un état valide.

Supposons qu'un agrégat Carutilise deux composants:

  • Transmission,
  • Engine.

Les deux Transmissionet les Engineobjets de valeur sont représentés comme types de super et ont selon les types de sous, Automaticet les Manualtransmissions, ou Petrolet Electricmoteurs respectivement.

Dans ce domaine, vivre seul a réussi à créer Transmission, que ce Automaticsoit ou Manual, ou l'un ou l'autre type d'un Engineest tout à fait bien. Mais l' Caragrégat introduit quelques nouvelles règles, applicables uniquement lorsque Transmissionet les Engineobjets sont utilisés dans le même contexte. À savoir:

  • Lorsqu'une voiture utilise un Electricmoteur, le seul type de transmission autorisé est Automatic.
  • Lorsqu'une voiture utilise un Petrolmoteur, elle peut avoir l'un ou l'autre type Transmission.

Je pourrais attraper cette violation de combinaison de composants au niveau de la création d'une commande, mais comme je l'ai déjà dit, d'après ce que je comprends, cela ne devrait pas être fait car la commande contiendrait alors une logique métier qui devrait être limitée à la couche domaine.

L'une des options est de déplacer cette validation de logique métier pour commander le validateur lui-même, mais cela ne semble pas non plus être correct. J'ai l'impression de déconstruire la commande, de vérifier ses propriétés récupérées à l'aide de getters et de les comparer dans le validateur et d'inspecter les résultats. Cela me crie comme une violation de la loi de Déméter .

En ignorant l'option de validation mentionnée car elle ne semble pas viable, il semble que l'on devrait utiliser la commande et en construire l'agrégat. Mais où cette logique devrait-elle exister? Doit-il appartenir au gestionnaire de commandes chargé de gérer une commande concrète? Ou devrait-il être dans le validateur de commandes (je n'aime pas non plus cette approche)?

J'utilise actuellement une commande et j'en crée un agrégat dans le gestionnaire de commandes responsable. Mais quand je fais cela, si j'avais un validateur de commande, il ne contiendrait rien du tout, car si la CreateCarcommande existait, elle contiendrait alors des composants dont je sais qu'ils sont valides dans des cas distincts mais l'agrégat pourrait dire différent.


Imaginons un scénario différent mélangeant différents processus de validation - création d'un nouvel utilisateur à l'aide d'une CreateUsercommande.

La commande contient un Iddes utilisateurs qui auront été créés et leur Email.

Le système définit les règles suivantes pour l'adresse e-mail de l'utilisateur:

  • doit être unique,
  • ne doit pas être vide,
  • doit contenir au plus 100 caractères (longueur maximale d'une colonne db).

Dans ce cas, même si avoir un e-mail unique est une règle commerciale, le vérifier dans son ensemble n'a pas beaucoup de sens, car je devrais charger l'ensemble des e-mails actuels dans le système dans une mémoire et vérifier l'e-mail dans la commande contre l'agrégat ( Eeeek! Quelque chose, quelque chose, la performance.). Pour cette raison, je déplacerais cette vérification vers le validateur de commande, qui prendrait UserRepositorycomme dépendance et utiliserait le référentiel pour vérifier si un utilisateur avec l'e-mail présent dans la commande existe déjà.

Quand il s'agit de cela, il est soudain logique de mettre également les deux autres règles de messagerie dans le validateur de commande. Mais j'ai le sentiment que les règles doivent être réellement présentes dans un Useragrégat et que le validateur de commande ne doit vérifier que l'unicité et si la validation réussit, je dois continuer à créer l' Useragrégat dans le CreateUserCommandHandleret le transmettre à un référentiel pour être enregistré.

Je me sens comme ça parce que la méthode de sauvegarde du référentiel est susceptible d'accepter un agrégat qui garantit qu'une fois l'agrégat passé, tous les invariants sont remplis. Lorsque la logique (par exemple, la non-vacuité) n'est présente que dans la validation de commande elle-même, un autre programmeur peut ignorer complètement cette validation et appeler directement la méthode de sauvegarde dans UserRepositoryavec un Userobjet, ce qui pourrait entraîner une erreur de base de données fatale, car l'e-mail peut avoir trop longtemps.

Comment gérez-vous personnellement ces validations et transformations complexes? Je suis surtout satisfait de ma solution, mais j'ai l'impression d'avoir besoin d'affirmer que mes idées et mes approches ne sont pas complètement stupides pour être assez satisfaites des choix. Je suis entièrement ouvert à des approches complètement différentes. Si vous avez quelque chose que vous avez personnellement essayé et qui a très bien fonctionné pour vous, j'aimerais voir votre solution.


1 En tant que développeur PHP responsable de la création de systèmes RESTful, mon interprétation de CQRS s'écarte un peu de l' approche standard de traitement des commandes asynchrones , comme le retour parfois des résultats de commandes en raison du besoin de traiter les commandes de manière synchrone.

Andy
la source
besoin d'un exemple de code je pense. à quoi ressemblent vos objets de commande et où les créez-vous?
Ewan
@Ewan J'ajouterai des exemples de code plus tard dans la journée ou demain. Partir en voyage dans quelques minutes.
Andy
En tant que programmeur PHP, je suggère de jeter un œil à mon implémentation CQRS + ES: github.com/xprt64/cqrs-es
Constantin Galbenu
@ConstantinGALBENU Si nous considérons que l'interprétation de Greg Young du CQRS est juste (ce que nous devrions probablement), alors votre compréhension du CQRS est fausse - ou du moins votre implémentation PHP l'est. Les commandes ne doivent pas être traitées directement par les agrégats. Les commandes doivent être gérées par des gestionnaires de commandes qui peuvent produire des modifications dans les agrégats qui produisent ensuite des événements à utiliser pour les réplications d'état.
Andy
Je ne pense pas que nos interprétations soient différentes. Il vous suffit de creuser davantage dans DDD (au niveau tactique des agrégats) ou d'ouvrir plus les yeux. Il existe au moins deux styles d'implémentation de CQRS. J'en utilise un. Mon implémentation ressemble davantage au modèle Actor et rend la couche Application très fine, ce qui est toujours une bonne chose. J'ai observé qu'il y avait beaucoup de duplication de code à l'intérieur de ces services d'application et j'ai décidé de les remplacer par un CommandDispatcher.
Constantin Galbenu

Réponses:

22

La réponse suivante est dans le contexte du style CQRS promu par le cqrs.nu dans lequel les commandes arrivent directement sur les agrégats. Dans ce style architectural, les services d'application sont remplacés par un composant d'infrastructure ( CommandDispatcher ) qui identifie l'agrégat, le charge, lui envoie la commande, puis persiste l'agrégat (sous la forme d'une série d'événements si le sourçage d'événements est utilisé).

La vraie question est donc: où est exactement la logique?

Il existe plusieurs types de logique (de validation). L'idée générale est d'exécuter la logique le plus tôt possible - échouez rapidement si vous le souhaitez. Ainsi, les situations sont les suivantes:

  • la structure de l'objet de commande lui-même; le constructeur de la commande a des champs obligatoires qui doivent être présents pour que la commande soit créée; c'est la première et la plus rapide validation; cela est évidemment contenu dans la commande.
  • validation de champ de bas niveau, comme le non-vide de certains champs (comme le nom d'utilisateur) ou le format (une adresse e-mail valide). Ce type de validation doit être contenu dans la commande elle-même, dans le constructeur. Il existe un autre style d'avoir une isValidméthode, mais cela me semble inutile car quelqu'un devrait se rappeler d'appeler cette méthode alors qu'en fait une instanciation de commande réussie devrait suffire.
  • des command validatorsclasses distinctes qui ont la responsabilité de valider une commande. J'utilise ce type de validation lorsque j'ai besoin de vérifier des informations provenant de plusieurs agrégats ou sources externes. Vous pouvez l'utiliser pour vérifier l'unicité d'un nom d'utilisateur. Command validatorspourrait avoir des dépendances injectées, comme des référentiels. Gardez à l'esprit que cette validation est finalement cohérente avec l'agrégat (c'est-à-dire lorsque l'utilisateur est créé, un autre utilisateur avec le même nom d'utilisateur pourrait être créé entre-temps)! N'essayez pas non plus de mettre ici une logique qui devrait résider à l'intérieur de l'agrégat! Les validateurs de commandes sont différents des gestionnaires Sagas / Process qui génèrent des commandes basées sur des événements.
  • les méthodes d'agrégation qui reçoivent et traitent les commandes. Il s'agit de la dernière (sorte de) validation qui se produit. L'agrégat extrait les données de la commande et utilise une logique métier de base qu'il accepte (il modifie son état) ou la rejette. Cette logique est vérifiée d'une manière fortement cohérente. Ceci est la dernière ligne de défense. Dans votre exemple, la règle When a car uses Electric engine the only allowed transmission type is Automaticdoit être vérifiée ici.

Je me sens comme ça parce que la méthode de sauvegarde du référentiel est susceptible d'accepter un agrégat qui garantit qu'une fois l'agrégat passé, tous les invariants sont remplis. Lorsque la logique (par exemple, la non-vacuité) n'est présente que dans la validation de commande elle-même, un autre programmeur peut ignorer complètement cette validation et appeler directement la méthode de sauvegarde dans UserRepository avec un objet User, ce qui pourrait entraîner une erreur fatale dans la base de données, car l'e-mail cela aurait pu être trop long.

En utilisant les techniques ci-dessus, personne ne peut créer de commandes non valides ou contourner la logique à l'intérieur des agrégats. Les validateurs de commandes sont automatiquement chargés + appelés par le CommandDispatcherafin que personne ne puisse envoyer une commande directement à l'agrégat. On pourrait appeler une méthode sur l'agrégat en passant une commande mais ne pourrait pas persister les changements donc il serait inutile / inoffensif de le faire.

En tant que développeur PHP responsable de la création de systèmes RESTful, mon interprétation de CQRS s'écarte un peu de l'approche standard de traitement des commandes asynchrones, comme le retour parfois des résultats de commandes en raison du besoin de traiter les commandes de manière synchrone.

Je suis également programmeur PHP et je ne retourne rien de mes gestionnaires de commandes (méthodes agrégées dans le formulaire handleSomeCommand). Cependant, je retourne assez souvent des informations au client / navigateur dans HTTP response, par exemple l'ID de la racine d'agrégat nouvellement créée ou quelque chose d'un modèle de lecture, mais je ne retourne jamais (vraiment jamais ) quoi que ce soit de mes méthodes de commande d'agrégation. Le simple fait que la commande a été acceptée (et traitée - nous parlons de traitement PHP synchrone, non?!) Est suffisant.

Nous renvoyons quelque chose au navigateur (et faisons toujours CQRS par le livre) parce que CQRS n'est pas une architecture de haut niveau .

Un exemple du fonctionnement des validateurs de commandes:

Chemin de la commande à travers les validateurs de commande en route vers l'agrégat

Constantin Galbenu
la source
En ce qui concerne votre stratégie de validation, le point numéro deux me saute aux yeux comme un endroit probable où la logique sera souvent dupliquée. Certes, on voudrait que l'agrégat utilisateur valide un e-mail non vide et bien formé non? Cela devient évident lorsque nous introduisons une commande ChangeEmail.
king-side-slide
@ king-side-slide pas si vous avez un EmailAddressobjet valeur qui se valide.
Constantin Galbenu
C'est tout à fait exact. On pourrait encapsuler un EmailAddressafin de réduire les doublons. Mais plus important encore, ce faisant, vous déplaceriez également la logique de votre commande vers votre domaine. Il convient de noter que cela peut être poussé trop loin. Souvent, des connaissances similaires (objets de valeur) peuvent avoir des exigences de validation différentes en fonction de qui les utilise. EmailAddressest un exemple pratique car toute la conception de cette valeur a des exigences de validation globale.
king-side-slide
De même, l'idée d'un "validateur de commandes" semble inutile. Le but n'est pas d'empêcher la création et l'envoi de commandes invalides. Le but est de les empêcher de s'exécuter. Par exemple, je peux transmettre toutes les données que je veux avec une URL. Si elle n'est pas valide, le système rejette ma demande. La commande est toujours créée et envoyée. Si une commande nécessite plusieurs agrégats pour la validation (c'est-à-dire une collection d'utilisateurs pour vérifier l'unicité des e-mails), un service de domaine est mieux adapté. Des objets comme "x validator" sont souvent le signe d'un modèle anémique où les données sont séparées du comportement.
king-side-slide
1
@ king-side-slide Un exemple concret est UserCanPlaceOrdersOnlyIfHeIsNotLockedValidator. Vous pouvez voir qu'il s'agit d'un domaine distinct de celui des commandes, il ne peut donc pas être validé par le OrderAggregate lui-même.
Constantin Galbenu
6

Une prémisse fondamentale de DDD est que les modèles de domaine se valident eux-mêmes. Il s'agit d'un concept essentiel car il élève votre domaine en tant que partie responsable de la mise en œuvre de vos règles métier. Il garde également votre modèle de domaine au centre du développement.

Un système CQRS (comme vous le signalez correctement) est un détail d'implémentation représentant un sous-domaine générique qui implémente son propre mécanisme de cohésion. Votre modèle ne doit en aucun cas dépendre d' un élément quelconque de l'infrastructure CQRS pour se comporter conformément à vos règles métier. L'objectif de DDD est de modéliser le comportement d'un système de sorte que le résultat soit une abstraction utile des exigences fonctionnelles de votre domaine d'activité principal. Le fait de déplacer n'importe quel élément de ce comportement hors de votre modèle, aussi tentant soit-il, réduit l'intégrité et la cohésion de votre modèle (et le rend moins utile).

En étendant simplement votre exemple pour inclure une ChangeEmailcommande, nous pouvons parfaitement illustrer pourquoi vous ne voulez aucune de votre logique métier dans votre infrastructure de commande car vous auriez besoin de dupliquer vos règles:

  • l'e-mail ne peut pas être vide
  • l'e-mail ne doit pas dépasser 100 caractères
  • l'e-mail doit être unique

Alors maintenant que nous pouvons être sûrs que notre logique doit être dans notre domaine, abordons la question du "où". Les deux premières règles peuvent être facilement appliquées à notre Useragrégat, mais cette dernière règle est un peu plus nuancée; celui qui nécessite un approfondissement des connaissances pour obtenir un aperçu plus approfondi. À première vue, il peut sembler que cette règle s'applique à un User, mais ce n'est vraiment pas le cas. Le "caractère unique" d'un e-mail s'applique à une collection de Users(selon une certaine portée).

Ah ha! Dans cet esprit, il devient tout à fait clair que votre UserRepository(votre collection en mémoire de Users) peut être un meilleur candidat pour appliquer cet invariant. La méthode "save" est probablement l'endroit le plus raisonnable pour inclure la vérification (où vous pouvez lever une UserEmailAlreadyExistsexception). Alternativement, un domaine UserServicepourrait être chargé de créer de nouveaux Usersattributs et de mettre à jour leurs attributs.

L'échec rapide est une bonne approche, mais ne peut être fait que là et quand il s'intègre au reste du modèle. Il peut être extrêmement tentant de vérifier les paramètres d'une méthode (ou commande) de service d'application avant de poursuivre le traitement pour tenter de détecter les échecs lorsque vous (le développeur) savez que l'appel échouera quelque part plus profondément dans le processus. Mais ce faisant, vous aurez des connaissances dupliquées (et divulguées) d'une manière qui nécessitera probablement plus d'une mise à jour du code lorsque les règles métier changent.

glissière côté roi
la source
2
Je suis d'accord avec ça. Ma lecture jusqu'à présent (sans CQRS) me dit que la validation doit toujours aller dans le modèle de domaine pour protéger les invariants. Maintenant que je lis CQRS, cela me dit de mettre la validation dans les objets Command. Cela semble contre-intuitif. Connaissez-vous des exemples, par exemple sur GitHub, où la validation est placée dans le modèle de domaine au lieu de la commande? +1.
w0051977