web-dev-qa-db-fra.com

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

J'adapte le pauvre homme [[# #]] cqrs [~ # ~]1 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, une 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ù exactement 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, lorsque je modélise des objets de domaine, j'aime suivre le paradigme fail-fast , savoir quand un objet atteint une méthode, il est dans un état valide.

Supposons qu'un agrégat Car utilise deux composants:

  • Transmission,
  • Engine.

Les objets de valeur Transmission et Engine sont représentés comme des super types et ont des sous-types, Automatic et Manual transmissions, ou Petrol et Electric moteurs respectivement.

Dans ce domaine, vivre seul un Transmission créé avec succès, que ce soit Automatic ou Manual, ou l'un ou l'autre type d'un Engine est tout à fait correct. Mais l'agrégat Car introduit quelques nouvelles règles, applicables uniquement lorsque les objets Transmission et Engine sont utilisés dans le même contexte. À savoir:

  • Lorsqu'une voiture utilise le moteur Electric, le seul type de transmission autorisé est Automatic.
  • Lorsqu'une voiture utilise le moteur Petrol, elle peut avoir l'un ou l'autre type de 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'ai un validateur de commande, il ne contiendra rien du tout, car si la commande CreateCar existe, 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 commande CreateUser.

La commande contient un Id d'un utilisateur qui aura été créé et son Email.

Le système énonce 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 UserRepository comme 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 agrégat User 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'agrégat User dans le CreateUserCommandHandler et le transmettre à un référentiel pour être enregistré.

Je ressens cela 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 le UserRepository avec un objet User qui pourrait conduire à une erreur de base de données fatale, car l'e-mail était peut-être trop long.

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 Travaillant en tant que PHP responsable de la création de systèmes RESTful, mon interprétation de CQRS s'écarte un peu de l'approche standard async-command-processing, comme le retour de résultats parfois des commandes en raison de la nécessité de traiter les commandes de manière synchrone.

22
Andy

La réponse suivante est dans le contexte du style CQRS promu par le cqrs.n 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 (le CommandDispatcher ) qui identifie l'agrégat, le charge, lui envoie la commande, puis persiste l'agrégat (comme une série d'événements si Event le sourcing 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 méthode isValid 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.
  • séparé command validators, classes 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 validators pourrait 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 commande 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 Automatic doit être vérifié ici.

Je ressens cela 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 peut entraîner une erreur fatale de 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 invalides ou contourner la logique à l'intérieur des agrégats. Les validateurs de commande sont automatiquement chargés + appelés par le CommandDispatcher afin 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.

Travaillant 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 asynchrone des commandes, comme le retour parfois des résultats de commandes en raison de la nécessité de traiter les commandes de façon synchrone.

Je suis aussi un programmeur PHP et je ne retourne rien de mes gestionnaires de commandes (méthodes agrégées sous la forme handleSomeCommand). Cependant, je retourne assez souvent informations au client/navigateur dans le 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 ) rien de mon agrégat méthodes de commande. Le simple fait que la commande a été acceptée (et traitée - nous parlons de synchrone PHP traitement, 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 nivea .

Un exemple du fonctionnement des validateurs de commandes:

Command's path through command validators on its way to the Aggregate

22

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 faites remarquer à juste titre) 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 de tout élément d'infrastructure CQRS pour se comporter selon vos règles métier. Le but de DDD est de modéliser le comportement d'un système de telle 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 commande ChangeEmail, 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 agrégat User, 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 évident 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 exception UserEmailAlreadyExists). Alternativement, un domaine UserService pourrait être chargé de créer de nouveaux Users 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.

6
king-side-slide