web-dev-qa-db-fra.com

La commande CQRS doit-elle effectuer une requête?

J'ai Customer, Order et Product Racines agrégées ... Lorsque la commande est créée, elle prend un Customer et un List<Sales.Items>. Cela ressemble à quelque chose comme:

public class Order
{
    public static Order Create(Customer customer, List<Sales.Item> items)
    {
        // Creates the order
        return newOrder;
    }
}

Ensuite, en utilisant CQRS, j'ai créé OrderCreateHandler qui ressemble à quelque chose comme:

public class OrderCreateCommandHandler : IRequestHandler<OrderCreateCommand>
{

    public OrderCreateCommandHandler(ECommerceContext db)
    {
        _db = db;
    }

    public async Task<Unit> Handle(OrderCreateCommand request, CancellationToken cancellationToken)
    {
        var customerResult = // Q) Is it okay to execute a CustomerQuery here?

        var customer = new Customer(customerResult.Id, customerResult.FirstName, customerResult.MiddleName,
            customerResult.LastName, customerResult.StreetAddress, customerResult.City, customerResult.State, "United States",
            customerResult.ZipCode);

         //  blah....

         order = Order.Create(customer, products);
         _db.Orders.Add(order);
     }
}

Ma question se trouve dans le gestionnaire de commandes, est-il correct d'effectuer des requêtes pour obtenir les données afin de créer les racines agrégées dont je dois ensuite passer? Je ne stocke jamais la racine agrégée elle-même (juste une référence si j'ai besoin), mais je ne veux pas passer d'ID et de primitives partout, je veux suivre SOLID OO et passez des objets réels. Est-ce une violation du CQRS?

6
keelerjr12

Commençons par un bref examen de l'espace des problèmes ici. L'avantage fondamental de l'adoption d'un modèle CQRS est de résoudre/simplifier votre domaine de problème en réduisant l'entrelacement et les fuites qui commencent à se produire lors de l'utilisation du même modèle pour votre côté écriture comme votre côté lecture. Souvent, la tension qui survient nuit aux deux. Le CQRS cherche à soulager cette tension en séparant (découplant logiquement et éventuellement physiquement) le côté écriture et le côté lecture de votre système. Compte tenu de ce qui précède, il doit être clair que ni vos commandes ni vos requêtes ne doivent être couplées à une entité logique de l'autre "côté".

Compte tenu de ce qui précède, nous pouvons maintenant formuler une réponse directe à votre question: Oui, vous pouvez interroger votre magasin de données dans un gestionnaire de commandes fourni la requête est émise contre votre modèle de commande. Parce que votre OrderCreateCommandHandler fait partie du modèle de commande de votre application, nous voulons éviter de le coupler à n'importe quelle partie de votre modèle de lecture. Il n'est pas clair si c'est le cas compte tenu de l'exemple de code (bien que le nom CustomerQuery soulève certains soupçons).

Plus important que la réponse ci-dessus, c'est que ... il y a quelque chose d'autre qui semble louche à propos de l'exemple que vous avez fourni. Le ressentez-vous aussi?

Ce que je vois ici est un peu de couplage. Votre gestionnaire récupère un CustomerResult (VO?), Puis décompose toutes ses données dans le constructeur d'une autre entité (Customer), puis transmet le Customer à une méthode d'usine de encore une autre entité. Nous avons pas mal de "demandes" qui se passent ici. Autrement dit, nous transmettons beaucoup de données de manière à créer un couplage.

De plus, le gestionnaire de commandes ne "lit" pas de manière très déclarative (c'est ce que nous voulons rechercher). Ce que je veux dire, c'est qu'il est un peu difficile de "voir" ce qui se passe dans votre méthode car il y a tellement de plomberie qui vous gêne. Je pense que nous pouvons trouver une solution plus cohérente/déclarative.

Étant donné que le "flux" général d'un gestionnaire de commandes peut être décomposé en trois étapes simples:

  1. Récupérer toutes les données (modèle de domaine) nécessaires à la réalisation du cas d'utilisation
  2. Coordonner les données pour répondre au cas d'utilisation
  3. Persister les données

Voyons si nous pouvons trouver une solution plus simple:

buyer = buyers.Find( cmd.CustomerId );

buyer.PlaceOrder( cmd.Products );

buyers.Save( buyer );

Ah ha! Beaucoup plus propre (3 étapes simples). Plus important encore, non seulement le code ci-dessus atteint votre même objectif, mais il le fait sans créer de nombreuses dépendances entre des objets disparates et fonctionner de manière plus déclarative et de manière encapsulée (nous ne "renouvelons" rien et n'appelons aucune méthode d'usine)! Décomposons cela morceau par morceau afin que nous puissions comprendre "pourquoi" ce qui précède peut être une meilleure solution.

buyer = buyers.Find( cmd.CustomerId );

La première chose que j'ai faite est d'introduire un nouveau concept: Buyer. Ce faisant, je partitionne vos données verticalement selon le comportement. Laissons votre entité Customer responsable de la maintenance des informations Customer (FirstName, LastName, Email, etc.) et autorisons un Buyer pour être responsable des achats. Étant donné que certaines informations Customer doivent être enregistrées lors d'un achat, nous hydraterons un Buyer avec un "instantané" de ces données (et éventuellement d'autres données).

buyer.PlaceOrder( cmd.Products );

Ensuite, nous coordonnons l'achat. La méthode ci-dessus est l'endroit où un new Order Est créé. Un Order n'apparaît pas juste de nulle part, non? Quelque chose doit le placer, nous modélisons donc en conséquence. Qu'est-ce que cela permet? Eh bien, la méthode Buyer.PlaceOrder Fournit une place dans votre domaine pour lancer BuyerNotInGoodStanding, OrderExceedsBuyerSpendingLimit, ou RepeatOrderDetected exceptions. En créant uniquement un Order dans le contexte de son placement, nous pouvons imposer comment un Order peut se produire. Dans votre exemple, votre gestionnaire de commandes de couche application ou votre méthode d'usine Order devrait être responsable de l'application de chaque invariant. Ce n'est pas non plus un bon endroit pour vérifier les règles commerciales. De plus, nous avons maintenant un endroit pour déclencher notre événement OrderPlaced (qui sera nécessaire pour maintenir le découplage de votre contexte de paiement), et nous pouvons également simplifier votre entité Order car elle n'a désormais besoin que d'un scalaire buyerId pour conserver la référence à son propriétaire.

buyers.Save( buyer );

Assez explicite. Un Buyer contient désormais toutes les informations dont vous avez besoin pour conserver à la fois un Order et un "instantané" des données Customer. La façon dont vous organisez ces données en interne et les démontez pour la persistance dépend de vous (indice: un Buyer n'a pas du tout besoin d'être conservé, par exemple. Juste le Order qu'il contient).

MODIFIER

L'exemple de solution (si nous pouvons l'appeler ainsi) que j'ai posté est destiné à faire tourner les "engrenages", et ne représente pas nécessairement le meilleure solution possible au problème en question. Autrement dit, votre problème . Il est tout à fait possible (voire probable) que l'introduction du concept d'un agrégat Buyer soit une ingénierie excessive étant donné qu'aucune sorte de règle n'a été mentionnée sur la manière de placer un Order. Par exemple:

customer = customers.Find( cmd.CustomerId );

order = customer.PlaceOrder( cmd.Products ); // raise OrderPlaced

orders.Save( order );

peut être une approche totalement valable! Assurez-vous simplement d'inclure toutes les informations nécessaires dans le CustomerInformationSlip (votre "instantané") attaché au Order pour lui permettre d'appliquer tout invariant contrôlant comment il peut être modifié. Par exemple:

order.ChangeShippingAddress( cmd.Address ); // raise ShippingAddressChanged

Ce qui précède peut lancer un OrderExceedsCustomerShippingDistance si chaque Customer a ses propres règles concernant la distance à laquelle vous leur expédierez compte tenu de leur niveau de compte.

Laissez les règles dicter le design!

17
king-side-slide

Est-il correct d'effectuer des requêtes pour obtenir les données nécessaires à la création des racines agrégées que je dois ensuite transmettre?

Êtes-vous inquiet des courses de données?

Si vous n'êtes pas inquiet à propos d'une course aux données, vous pouvez utiliser en toute sécurité une copie périmée des données réelles, et le CQRS ne se soucie pas vraiment de la provenance des données périmées.

D'un autre côté, si une course aux données pose problème, vous devrez probablement arrêter et repenser les choses.

Dans ce cas spécifique, vous êtes probablement d'accord pour utiliser une requête; Je suppose que parce que vous semblez rechercher des informations client que vous ne contrôlez pas - ce que vous interrogez, ce sont des informations contrôlées par le monde réel. Vos données peuvent donc déjà être erronées (le client a changé de nom, mais vous n'avez pas encore reçu de notification).

1
VoiceOfUnreason