Dans la définition de wikipedia de séparation des requêtes de commande , il est indiqué que
Plus formellement, les méthodes ne doivent renvoyer une valeur que si elles sont référentiellement transparentes et ne présentent donc aucun effet secondaire.
Si j'émets une commande, comment dois-je déterminer ou signaler si cette commande a réussi, car selon cette définition, la fonction ne peut pas renvoyer de données?
Par exemple:
string result = _storeService.PurchaseItem(buyer, item);
Cet appel contient à la fois une commande et une requête, mais la partie requête est le résultat de la commande. Je suppose que je pourrais refactoriser cela en utilisant le modèle de commande, comme ceci:
PurchaseOrder order = CreateNewOrder(buyer, item);
_storeService.PerformPurchase(order);
string result = order.Result;
Mais cela semble augmenter la taille et la complexité du code, ce qui n'est pas une direction très positive vers une refactorisation.
Quelqu'un peut-il me donner un meilleur moyen de réaliser la séparation commande-requête lorsque vous avez besoin du résultat d'une opération?
Est-ce que j'ai râté quelque chose?
Merci!
Notes: Martin Fowler a ceci à dire sur les limites des cqs CommandQuerySeparation :
Meyer aime absolument utiliser la séparation commande-requête, mais il y a des exceptions. Popping une pile est un bon exemple d'un modificateur qui modifie l'état. Meyer dit à juste titre que vous pouvez éviter d'avoir cette méthode, mais c'est un idiome utile. Je préfère donc suivre ce principe quand je le peux, mais je suis prêt à le casser pour obtenir ma pop.
De son point de vue, cela vaut presque toujours la peine de refactoriser la séparation commande/requête, à l'exception de quelques exceptions simples mineures.
Cette question est ancienne mais n'a pas encore reçu de réponse satisfaisante, je vais donc développer un peu mon commentaire d'il y a près d'un an.
L'utilisation d'une architecture événementielle a beaucoup de sens, non seulement pour réaliser une séparation claire des commandes/requêtes, mais aussi parce qu'elle ouvre de nouveaux choix architecturaux et correspond généralement à un modèle de programmation asynchrone (utile si vous avez besoin de faire évoluer votre architecture). Plus souvent qu'autrement, vous constaterez que la solution peut résider dans la modélisation de votre domaine différemment.
Prenons donc votre exemple d'achat. StoreService.ProcessPurchase
serait une commande appropriée pour traiter un achat. Cela générerait un PurchaseReceipt
. C'est une meilleure solution que de renvoyer le reçu dans Order.Result
. Pour garder les choses très simples, vous pouvez retourner le reçu de la commande et violer CQRS ici. Si vous voulez une séparation plus nette, la commande déclenche un événement ReceiptGenerated
auquel vous pouvez vous abonner.
Si vous pensez à votre domaine, cela peut en fait être un meilleur modèle. Lorsque vous passez à la caisse, vous suivez ce processus. Avant que votre reçu ne soit généré, un chèque de carte de crédit peut être dû. Cela prendra probablement plus de temps. Dans un scénario synchrone, vous attendriez à la caisse, incapable de faire autre chose.
Je vois beaucoup de confusion ci-dessus entre CQS et CQRS (comme Mark Rogers l'a également remarqué lors d'une réponse).
CQRS est une approche architecturale dans DDD où, en cas de requête, vous ne créez pas de graphiques d'objets complets à partir de racines agrégées avec toutes leurs entités et types de valeur, mais uniquement des objets de vue légers à afficher dans une liste.
CQS est un bon principe de programmation au niveau du code dans n'importe quelle partie de votre application. Pas seulement la zone du domaine. Le principe existe bien plus longtemps que DDD (et CQRS). Il dit de ne pas gâcher les commandes qui changent n'importe quel état de l'application avec des requêtes qui renvoient juste des données et peuvent être invoquées à tout moment sans changer aucun état. Dans mes vieux jours avec Delphi, le calme montrait une différence entre les fonctions et les procédures. Il a été considéré comme une mauvaise pratique de coder les "procédures de fonction" comme nous les avons rappelées.
Pour répondre à la question posée: On pourrait penser à un moyen de contourner l'exécution d'une commande et de récupérer un résultat. Par exemple, en fournissant un objet de commande (modèle de commande) qui a une méthode d'exécution vide et une propriété de résultat de commande en lecture seule.
Mais quelle est la principale raison d'adhérer au CQS? Gardez le code lisible et réutilisable sans avoir à regarder les détails de l'implémentation. Votre code doit être fiable pour ne pas provoquer d'effets secondaires inattendus. Donc, si la commande veut renvoyer un résultat et que le nom de la fonction ou l'objet de retour indique clairement qu'il s'agit d'une commande avec un résultat de commande, j'accepte l'exception à la règle CQS. Pas besoin de rendre les choses plus complexes. Je suis d'accord avec Martin Fowler (mentionné ci-dessus) ici.
Soit dit en passant: ne pas suivre strictement cette règle ne briserait-il pas tout le principe de l'api fluide?
Eh bien, c'est une question assez ancienne mais je la poste juste pour mémoire. Chaque fois que vous utilisez un événement, vous pouvez utiliser à la place un délégué. Utilisez des événements si vous avez de nombreuses parties intéressées, sinon utilisez un délégué dans un style de rappel:
void CreateNewOrder(Customer buyer, Product item, Action<Order> onOrderCreated)
vous pouvez également avoir un bloc pour le cas où l'opération a échoué
void CreateNewOrder(Customer buyer, Product item, Action<Order> onOrderCreated, Action<string> onOrderCreationFailed)
Cela diminue la complexité cyclomatique sur le code client
CreateNewOrder(buyer: new Person(), item: new Product(),
onOrderCreated: order=> {...},
onOrderCreationFailed: error => {...});
J'espère que cela aide toute âme perdue là-bas ...
La question étant; Comment appliquez-vous CQS lorsque vous avez besoin du résultat d'une commande?
La réponse est: non. Si vous souhaitez exécuter une commande et obtenir un résultat, vous n'utilisez pas CQS.
Cependant, la pureté dogmatique en noir et blanc pourrait être la mort de l'univers. Il y a toujours des cas Edge et des zones grises. Le problème est que vous commencez à créer des modèles qui sont une forme de CQS, mais plus de CQS pur.
Une monade est une possibilité. Au lieu que votre commande revienne nulle, vous pouvez retourner Monade. une Monade "vide" pourrait ressembler à ceci:
public class Monad {
private Monad() { Success = true; }
private Monad(Exception ex) {
IsExceptionState = true;
Exception = ex;
}
public static Monad Success() => new Monad();
public static Monad Failure(Exception ex) => new Monad(ex);
public bool Success { get; private set; }
public bool IsExceptionState { get; private set; }
public Exception Exception { get; private set; }
}
Vous pouvez maintenant avoir une méthode "Command" comme ceci:
public Monad CreateNewOrder(CustomerEntity buyer, ProductEntity item, Guid transactionGuid) {
if (buyer == null || string.IsNullOrWhiteSpace(buyer.FirstName))
return Monad.Failure(new ValidationException("First Name Required"));
try {
var orderWithNewID = ... Do Heavy Lifting Here ...;
_eventHandler.Raise("orderCreated", orderWithNewID, transactionGuid);
}
catch (Exception ex) {
_eventHandler.RaiseException("orderFailure", ex, transactionGuid); // <-- should never fail BTW
return Monad.Failure(ex);
}
return Monad.Success();
}
Le problème avec la zone grise est qu'elle est facilement exploitée. Mettre des informations de retour telles que le nouveau OrderID dans la Monade permettrait aux consommateurs de dire: "Oubliez d'attendre l'événement, nous avons l'ID ici !!!" De plus, toutes les commandes n'auraient pas besoin d'une monade. Vous devriez vraiment vérifier la structure de votre application pour vous assurer que vous avez vraiment atteint un cas Edge.
Avec une Monade, votre consommation de commandes pourrait maintenant ressembler à ceci:
//some function child in the Call Stack of "CallBackendToCreateOrder"...
var order = CreateNewOrder(buyer, item, transactionGuid);
if (!order.Success || order.IsExceptionState)
... Do Something?
Dans une base de code très loin. . .
_eventHandler.on("orderCreated", transactionGuid, out order)
_storeService.PerformPurchase(order);
Dans une interface graphique très loin. . .
var transactionID = Guid.NewGuid();
OnCompletedPurchase(transactionID, x => {...});
OnException(transactionID, x => {...});
CallBackendToCreateOrder(orderDetails, transactionID);
Maintenant, vous avez toutes les fonctionnalités et la propreté que vous souhaitez avec juste un peu de zone grise pour la Monade, mais ASSUREZ-VOUS que vous n'exposez pas accidentellement un mauvais motif à travers la Monade, donc vous limitez ce que vous pouvez en faire.
J'aime les suggestions d'architecture événementielle que d'autres ont données, mais je veux juste apporter un autre point de vue. Vous devez peut-être chercher pourquoi vous renvoyez réellement des données de votre commande. Avez-vous réellement besoin du résultat, ou pourriez-vous vous en sortir en levant une exception si elle échoue?
Je ne dis pas cela comme une solution universelle, mais le passage à un modèle plus fort "d'exception en cas d'échec" au lieu de "renvoyer une réponse" m'a beaucoup aidé à faire fonctionner la séparation dans mon propre code. Bien sûr, vous finissez par devoir écrire beaucoup plus de gestionnaires d'exceptions, c'est donc un compromis ... Mais c'est au moins un autre angle à considérer.
Je suis vraiment en retard, mais il y a quelques autres options qui n'ont pas été mentionnées (cependant, je ne sais pas si elles sont vraiment géniales):
Une option que je n'ai jamais vue auparavant consiste à créer une autre interface pour le gestionnaire de commandes à implémenter. Peut être ICommandResult<TCommand, TResult>
que le gestionnaire de commandes implémente. Ensuite, lorsque la commande normale s'exécute, elle définit le résultat sur le résultat de la commande et l'appelant extrait ensuite le résultat via l'interface ICommandResult. Avec IoC, vous pouvez le faire pour qu'il renvoie la même instance que le gestionnaire de commandes afin que vous puissiez extraire le résultat. Cependant, cela pourrait briser SRP.
Une autre option consiste à avoir une sorte de magasin partagé qui vous permet de mapper les résultats des commandes d'une manière qu'une requête pourrait ensuite récupérer. Par exemple, supposons que votre commande contenait un tas d'informations, puis un OperationId Guid ou quelque chose du genre. Lorsque la commande se termine et obtient le résultat, elle envoie la réponse à la base de données avec ce Guid OperationId comme clé ou une sorte de dictionnaire partagé/statique dans une autre classe. Lorsque l'appelant reprend le contrôle, il appelle une requête pour retirer en fonction du résultat basé sur le Guid donné.
La réponse la plus simple est de simplement pousser le résultat sur la commande elle-même, mais cela peut être déroutant pour certaines personnes. L'autre option que je vois mentionnée est les événements, ce que vous pouvez techniquement faire, mais si vous êtes dans un environnement Web, cela le rend beaucoup plus difficile à gérer.
Modifier
Après avoir travaillé avec cela plus, j'ai fini par créer un "CommandQuery". C'est un hybride entre commande et requête, évidemment. :) S'il y a des cas où vous avez besoin de cette fonctionnalité, vous pouvez l'utiliser. Cependant, il doit y avoir une très bonne raison de le faire. Il ne sera PAS reproductible et ne peut pas être mis en cache, il existe donc des différences par rapport aux deux autres.
Prenez un peu plus de temps pour réfléchir POURQUOI vous voulez la séparation des requêtes de commande.
"Il vous permet d'utiliser des requêtes à volonté sans vous soucier de changer l'état du système."
Il est donc OK de renvoyer une valeur à partir d'une commande pour faire savoir à l'appelant qu'elle a réussi
car il serait inutile de créer une requête distincte dans le seul but de
découvrir si une commande précédente fonctionnait correctement. Quelque chose comme ça va bien
mes livres:
boolean succeeded = _storeService.PurchaseItem(buyer, item);
Un inconvénient de votre exemple est qu’il n’est pas évident de savoir ce que votre
méthode.
string result = _storeService.PurchaseItem(buyer, item);
On ne sait pas exactement ce qu'est le "résultat".
L'utilisation de CQS (Command Query Seperation) vous permet de rendre les choses plus évidentes
similaire à ci-dessous:
if(_storeService.PurchaseItem(buyer, item)){
String receipt = _storeService.getLastPurchaseReciept(buyer);
}
Oui, c'est plus de code, mais ce qui se passe est plus clair.
CQS est principalement utilisé lors de l'implémentation de la conception pilotée par domaine, et vous devez donc (comme Oded le dit également) utiliser une architecture pilotée par les événements pour traiter les résultats. Votre string result = order.Result;
serait donc toujours dans un gestionnaire d'événements, et pas directement après dans le code.
Découvrez cet excellent article qui montre une combinaison de CQS, DDD et EDA.