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 Car
utilise deux composants:
Transmission
,Engine
.
Les deux Transmission
et les Engine
objets de valeur sont représentés comme types de super et ont selon les types de sous, Automatic
et les Manual
transmissions, ou Petrol
et Electric
moteurs respectivement.
Dans ce domaine, vivre seul a réussi à créer Transmission
, que ce Automatic
soit ou Manual
, ou l'un ou l'autre type d'un Engine
est tout à fait bien. Mais l' Car
agrégat introduit quelques nouvelles règles, applicables uniquement lorsque Transmission
et les Engine
objets sont utilisés dans le même contexte. À savoir:
- Lorsqu'une voiture utilise un
Electric
moteur, le seul type de transmission autorisé estAutomatic
. - Lorsqu'une voiture utilise un
Petrol
moteur, elle peut avoir l'un ou l'autre typeTransmission
.
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 CreateCar
commande 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 CreateUser
commande.
La commande contient un Id
des 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 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 User
agré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' User
agrégat dans le CreateUserCommandHandler
et 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 UserRepository
avec un User
objet, 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.
CommandDispatcher
.Réponses:
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é).
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:
isValid
mé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.command validators
classes 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 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 commandes sont différents des gestionnaires Sagas / Process qui génèrent des commandes basées sur des événements.When a car uses Electric engine the only allowed transmission type is Automatic
doit être vérifiée ici.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
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.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 dansHTTP 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:
la source
EmailAddress
objet valeur qui se valide.EmailAddress
afin 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.EmailAddress
est un exemple pratique car toute la conception de cette valeur a des exigences de validation globale.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.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
ChangeEmail
commande, 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: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
User
agré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 à unUser
, mais ce n'est vraiment pas le cas. Le "caractère unique" d'un e-mail s'applique à une collection deUsers
(selon une certaine portée).Ah ha! Dans cet esprit, il devient tout à fait clair que votre
UserRepository
(votre collection en mémoire deUsers
) 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 uneUserEmailAlreadyExists
exception). Alternativement, un domaineUserService
pourrait être chargé de créer de nouveauxUsers
attributs 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.
la source