web-dev-qa-db-fra.com

CQRS: Valeurs de retour de commande

Il semble y avoir une confusion sans fin quant à savoir si les commandes devraient ou non avoir des valeurs de retour. Je voudrais savoir si la confusion est simplement due au fait que les participants n’ont pas indiqué leur contexte ou leurs circonstances.

La confusion

Voici des exemples de la confusion ...

  • Udi Dahan dit que les commandes "ne renvoient pas d'erreurs au client", mais dans le même article il montre un diagramme où les commandes renvoient effectivement des erreurs au client.

  • Un article de Microsoft Press Store stipule que "la commande ... ne renvoie pas de réponse", mais poursuit par une mise en garde ambiguë:

À mesure que l'expérience du champ de bataille se développe autour du CQRS, certaines pratiques se consolident et tendent à devenir des pratiques exemplaires. En partie contraire à ce que nous venons de dire ... C’est une vision commune aujourd’hui de penser que le gestionnaire de commandes et l’application doivent savoir comment l’opération transactionnelle s’est déroulée. Les résultats doivent être connus ...

Les gestionnaires de commandes renvoient-ils des valeurs ou non?

La réponse?

Reprenant l'exemple de " Myths CQRS ", de Jimmy Bogard, je pense que la réponse à cette question dépend du "quadrant" programmatique/contextuel dont vous parlez:

+-------------+-------------------------+-----------------+
|             | Real-time, Synchronous  |  Queued, Async  |
+-------------+-------------------------+-----------------+
| Acceptance  | Exception/return-value* | <see below>     |
| Fulfillment | return-value            | n/a             |
+-------------+-------------------------+-----------------+

Acceptation (par exemple validation)

La commande "Acceptance" fait principalement référence à la validation. Les résultats de validation doivent vraisemblablement être communiqués de manière synchrone à l'appelant, que la commande "accomplissement" soit synchrone ou en file d'attente.

Cependant, il semble que de nombreux praticiens n'initient pas la validation à partir du gestionnaire de commandes. D'après ce que j'ai vu, c'est soit parce que (1) ils ont déjà trouvé un moyen fantastique de gérer la validation au niveau de la couche d'application (un contrôleur ASP.NET MVC vérifiant l'état valide via des annotations de données) ou (2) une architecture. est en place, ce qui suppose que les commandes sont soumises à un bus ou à une file d'attente (hors processus). Ces dernières formes d'asynchronisme n'offrent généralement pas de sémantique ou d'interfaces de validation synchrone.

En bref, de nombreux concepteurs peuvent souhaiter que le gestionnaire de commandes fournisse les résultats de la validation sous forme de valeur de retour (synchrone), mais ils doivent respecter les restrictions des outils asynchrones qu'ils utilisent.

Accomplissement

En ce qui concerne "l'exécution" d'une commande, le client qui a émis la commande peut avoir besoin de connaître le scope_identity pour un enregistrement nouvellement créé ou peut-être des informations d'échec, telles que "le compte à découvert".

En temps réel, il semble qu'une valeur de retour ait le plus de sens; les exceptions ne doivent pas être utilisées pour communiquer les résultats d'échec liés aux activités. Cependant, dans un contexte de "file d'attente" ... les valeurs de retour n'ont naturellement aucun sens.

C’est là que toute la confusion peut peut-être être résumée:

De nombreux praticiens CQRS (la plupart?) Partent du principe qu’ils vont maintenant, ou à l’avenir, incorporer un framework ou une plate-forme asynchrone (un bus ou une file d’attente) et proclamer ainsi que les gestionnaires de commandes n’ont pas de valeurs de retour. Cependant, certains praticiens n’ont pas l’intention d’utiliser de telles constructions pilotées par les événements, ils vont donc approuver les gestionnaires de commandes renvoyant des valeurs (de manière synchrone).

Ainsi, par exemple, je crois qu'un contexte synchrone (demande-réponse) a été supposé quand Jimmy Bogard a fourni cet exemple d'interface de commande :

public interface ICommand<out TResult> { }

public interface ICommandHandler<in TCommand, out TResult>
    where TCommand : ICommand<TResult>
{
    TResult Handle(TCommand command);
}

Son produit Mediatr est, après tout, un outil en mémoire. Compte tenu de tout cela, je pense que la raison pour laquelle Jimmy prenait soigneusement le temps de produire un retour nul d'une commande n'était pas parce que "les gestionnaires de commandes ne devraient pas avoir de valeurs de retour", mais plutôt parce qu'il voulait simplement sa classe Mediator d'avoir une interface cohérente:

public interface IMediator
{
    TResponse Request<TResponse>(IQuery<TResponse> query);
    TResult Send<TResult>(ICommand<TResult> query);  //This is the signature in question.
}

... même si toutes les commandes n'ont pas une valeur significative à renvoyer.

Répéter et terminer

Est-ce que je saisis correctement pourquoi il y a confusion sur ce sujet? Y a-t-il quelque chose qui me manque?

55
Brent Arias

À la suite des conseils donnés par Vladik Khononov dans Tackling Complexity in CQRS , le traitement des commandes peut renvoyer des informations relatives à ses résultats.

Sans violer les principes [CQRS], une commande peut renvoyer en toute sécurité les données suivantes:

  • Résultat d'exécution: succès ou échec;
  • Messages d'erreur ou erreurs de validation, en cas d'échec;
  • Le nouveau numéro de version de l’agrégat, en cas de succès;

Cette information améliorera considérablement l'expérience utilisateur de votre système, car:

  • Vous n’avez pas à interroger une source externe pour obtenir le résultat de l’exécution de la commande, vous l’avez tout de suite. Il devient trivial de valider des commandes et de renvoyer des messages d'erreur.
  • Si vous souhaitez actualiser les données affichées, vous pouvez utiliser la nouvelle version de l’agrégat pour déterminer si le modèle de vue reflète ou non la commande exécutée. Plus besoin d'afficher des données obsolètes.

Daniel Whittaker recommande de renvoyer un objet " résultat commun " à partir d'un gestionnaire de commandes contenant ces informations.

16
Ben Smith

Les gestionnaires de commandes renvoient-ils des valeurs ou non?

Ils ne doivent pas renvoyer Business Data , mais uniquement des métadonnées (concernant le succès ou l'échec de l'exécution de la commande). CQRS est CQS porté à un niveau supérieur. Même si vous enfreigniez les règles du puriste et rapportiez quelque chose, que reviendriez-vous? Dans CQRS, le gestionnaire de commandes est une méthode d'un application service qui charge le aggregate puis appelle une méthode sur le aggregate puis persiste le aggregate. L'intention du gestionnaire de commandes est de modifier le aggregate. Vous ne sauriez pas quoi retourner qui serait indépendant de l'appelant. Chaque appelant/client du gestionnaire de commandes voudrait savoir autre chose sur le nouvel état.

Si l'exécution de la commande est bloquante (c'est-à-dire synchrone), il vous suffira alors de savoir si la commande a été exécutée avec succès ou non. Ensuite, dans une couche supérieure, vous interrogerez la chose exacte que vous devez savoir sur l'état de la nouvelle application en utilisant un modèle de requête qui répond le mieux à vos besoins.

Si vous renvoyez quelque chose à partir d'un gestionnaire de commandes, vous lui donnez deux responsabilités: 1. modifier l'état d'agrégat et 2. interroger un modèle de lecture.

En ce qui concerne la validation de commande, il existe au moins deux types de validation de commande:

  1. contrôle d'intégrité de la commande, qui vérifie qu'une commande a les données correctes (c'est-à-dire qu'une adresse électronique est valide); ceci est fait avant que la commande n'atteigne l'agrégat, dans le gestionnaire de commandes (le service d'application) ou dans le constructeur de la commande;
  2. la vérification des invariants de domaine, effectuée dans l’agrégat, une fois que la commande a atteint l’agrégat (après l’appel d’une méthode sur l’agrégat) et que l’agrégat peut muter vers le nouvel état.

Cependant, si nous montons un peu plus haut, dans le Presentation layer _ (c'est-à-dire un REST endpoint), le client du Application layer, nous pouvons renvoyer n'importe quoi et nous ne violerons pas les règles, car les noeuds finaux sont conçus après les cas d'utilisation. Vous savez exactement ce que vous voulez renvoyer après l'exécution d'une commande, dans tous les cas d'utilisation.

5
Constantin Galbenu

Répondre pour @Constantin Galbenu, j'ai été limité.

@Misanthrope Et que faites-vous exactement avec ces événements?

@ Constantin Galbenu, dans la plupart des cas, je n'en ai évidemment pas besoin comme résultat de la commande. Dans certains cas, je dois informer le client en réponse à cette demande d'API.

C'est extrêmement utile quand:

  1. Vous devez notifier une erreur via des événements plutôt que des exceptions. Cela se produit généralement lorsque votre modèle doit être enregistré (par exemple, il compte le nombre de tentatives avec un code/mot de passe incorrect), même en cas d'erreur. De plus, certains gars n'utilisent pas du tout d'exceptions pour les erreurs commerciales - uniquement les événements ( http://andrzejonsoftware.blogspot.com/2014/06/custom-exceptions-or-domain-events.html =) Il n'y a pas de raison particulière de penser que le lancement d'exceptions métier à partir du gestionnaire de commandes est correct, mais que le renvoi d'événements de domaine ne l'est pas .
  2. Lorsque l'événement ne se produit qu'avec certaines circonstances à l'intérieur de votre racine agrégée.

Et je peux donner un exemple pour le second cas. Imaginons que nous fournissions un service similaire à Tinder, nous avons la commande LikeStranger. Cette commande PEUT avoir pour résultat StrangersWereMatched si nous aimons les personnes qui nous ont déjà aimés auparavant. Nous devons informer le client mobile en cas de correspondance ou non. Si vous voulez juste vérifier matchQueryService après la commande, vous pouvez trouver correspondance, mais il n'y a aucune garantie que la correspondance a eu lieu actuellement, parce que SOMETIMES Tinder montre des inconnus déjà appariés (probablement dans 2ème appareil, etc.).

Vérifier la réponse si StrangersWereMatched s'est vraiment passé maintenant est si simple:

$events = $this->commandBus->handle(new LikeStranger(...));

if ($events->contains(StrangersWereMatched::class)) {
  return LikeApiResponse::matched();
} else {
  return LikeApiResponse::unknown();
}

Oui, vous pouvez introduire l'ID de la commande, par exemple, et créer un modèle de lecture Match pour le conserver:

// ...

$commandId = CommandId::generate();

$events = $this->commandBus->handle(
  $commandId,
  new LikeStranger($strangerWhoLikesId, $strangerId)
);

$match = $this->matchQueryService->find($strangerWhoLikesId, $strangerId);

if ($match->isResultOfCommand($commandId)) {
  return LikeApiResponse::matched();
} else {
  return LikeApiResponse::unknown();
}

... mais réfléchissez-y: pourquoi pensez-vous que le premier exemple avec une logique simple est pire? De toute façon, cela ne viole pas le CQRS, je viens de rendre explicite l’explicite. C'est une approche immuable sans état. Moins de chances de rencontrer un bogue (par exemple, si matchQueryService est mis en cache/différé [pas instantanément cohérent], vous avez un problème).

Oui, lorsque le fait de mettre en correspondance ne suffit pas et que vous devez obtenir des données pour répondre, vous devez utiliser le service de requête. Mais rien ne vous empêche de recevoir des événements du gestionnaire de commandes.

1
Misanthrope

CQRS et CQS sont comme des microservices et une décomposition de classe: l'idée principale est la même ("ont tendance à être modeste", mais ils se situent à différents niveaux sémantiques.

Le but de CQRS est de séparer les modèles en écriture/lecture; de tels détails de bas niveau, tels que la valeur renvoyée par une méthode spécifique, sont totalement hors de propos.

Prenez note de ce qui suit citation de Fowler :

La modification introduite par CQRS consiste à scinder ce modèle conceptuel en modèles distincts pour la mise à jour et l'affichage, qu'il nomme Command et Query, respectivement, en suivant le vocabulaire de CommandQuerySeparation.

Il s’agit de modèles et non de méthodes .

Le gestionnaire de commandes peut renvoyer n'importe quoi à l'exception des modèles en lecture: statut (succès/échec), événements générés (le principal objectif des gestionnaires de commandes, btw: générer des événements pour la commande donnée), erreurs. Les gestionnaires de commandes jettent très souvent une exception non contrôlée, il s'agit d'un exemple de signaux de sortie des gestionnaires de commandes.

En outre, l'auteur du terme, Greg Young, indique que les commandes sont toujours synchronisées (sinon, cela devient un événement): https://groups.google.com/forum/#!topic/dddcqrs/xhJHVxDx2pM

Greg Young

en fait j'ai dit qu'une commande asynchrone n'existe pas :) c'est en fait un autre événement.

1
Misanthrope