it-swarm.com.de

Sollte der CQRS-Befehl eine Abfrage durchführen?

Ich habe Customer, Order und Product Aggregate Roots ... Wenn die Bestellung erstellt wird, werden ein Customer und ein List<Sales.Items>. Es sieht ungefähr so ​​aus:

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

Dann habe ich mit CQRS OrderCreateHandler erstellt, das ungefähr so ​​aussieht:

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);
     }
}

Meine Frage befindet sich im Befehlshandler. Ist es in Ordnung, Abfragen durchzuführen, um die Daten zum Erstellen der aggregierten Wurzeln zu erhalten, die ich dann übergeben muss? Ich speichere nie die aggregierte Wurzel selbst (nur eine Referenz, wenn ich sie brauche), aber ich möchte nicht überall IDs und Grundelemente übergeben, sondern SOLID OO und tatsächliche Objekte übergeben. Ist dies eine Verletzung von CQRS?

6
keelerjr12

Beginnen wir hier mit einem kurzen Überblick über den Problembereich. Der grundlegende Vorteil der Verwendung eines CQRS-Musters besteht darin, dass Sie Ihre Problemdomäne lösen/vereinfachen, indem Sie die Verschachtelung und Leckage reduzieren, die bei Verwendung desselben Modells für auftreten Ihre Schreibseite als Ihre Leseseite. Oft wirkt sich die entstehende Spannung nachteilig auf beide aus. CQRS versucht, diese Spannung zu lösen, indem es die Schreib- und Leseseite Ihres Systems trennt (logisch und möglicherweise physisch entkoppelt). Vor diesem Hintergrund sollte klar sein, dass weder Ihre Befehle noch Ihre Abfragen von der anderen "Seite" an eine logische Entität gekoppelt werden sollten.

Vor diesem Hintergrund können wir jetzt eine direkte Antwort auf Ihre Frage formulieren: Ja, Sie können Ihren Datenspeicher in einem Befehlshandler abfragen vorausgesetzt Die Abfrage wird für Ihr Befehlsmodell ausgegeben. Da Ihr OrderCreateCommandHandler Teil des Befehlsmodells Ihrer Anwendung ist, möchten wir vermeiden, dass es an einen Teil Ihres Lesemodells gekoppelt wird. Es ist unklar, ob dies angesichts des Beispielcodes der Fall ist oder nicht (obwohl der Name CustomerQuery einige Verdächtigungen aufwirft).

Wichtiger als die obige Antwort ist jedoch, dass ... es noch etwas gibt, das an dem von Ihnen angegebenen Beispiel faul ist. Kannst du das auch fühlen?

Was ich hier sehe, ist ziemlich viel Kopplung. Ihr Handler ruft ein CustomerResult (VO?) Ab, zerlegt dann alle Daten in den Konstruktor einer anderen Entität (Customer) und übergibt das Customer an eine Factory-Methode von noch eine andere Entität. Wir haben hier einiges zu "fragen". Das heißt, wir geben viele Daten so weiter, dass eine Kopplung entsteht.

Darüber hinaus "liest" der Befehlshandler nicht sehr deklarativ (was wir anstreben wollen). Was ich meine ist, dass es schwierig ist zu "sehen", was in Ihrer Methode passiert, weil so viel Wasser im Weg ist. Ich denke, wir können eine kohärentere/deklarativere Lösung finden.

Vorausgesetzt, der allgemeine "Ablauf" eines Befehlshandlers kann in drei einfache Schritte unterteilt werden:

  1. Rufen Sie alle Daten (Domänenmodell) ab, die zur Ausführung des Anwendungsfalls erforderlich sind
  2. Koordinieren Sie die Daten, um den Anwendungsfall zu erfüllen
  3. Behalten Sie die Daten bei

Lassen Sie uns sehen, ob wir eine einfachere Lösung finden können:

buyer = buyers.Find( cmd.CustomerId );

buyer.PlaceOrder( cmd.Products );

buyers.Save( buyer );

Ah ha! Viel sauberer (3 einfache Schritte). Noch wichtiger ist jedoch, dass der obige Code nicht nur dasselbe Ziel erreicht, sondern auch viele Abhängigkeiten zwischen unterschiedlichen Objekten erzeugt und in einem deklarativeren und gekapselte Weise (wir "erneuern" nichts oder rufen keine Factory-Methoden auf)! Lassen Sie uns dies Stück für Stück aufschlüsseln, damit wir verstehen, warum das oben Genannte eine bessere Lösung sein kann.

buyer = buyers.Find( cmd.CustomerId );

Als erstes habe ich ein neues Konzept eingeführt: Buyer. Dabei partitioniere ich Ihre Daten vertikal nach Verhalten. Lassen Sie Ihre Entität Customer für die Verwaltung von Customer - Informationen (FirstName, LastName, Email usw.) verantwortlich sein und erlauben Sie a Buyer, um für Einkäufe verantwortlich zu sein. Da einige Customer - Informationen beim Kauf aufgezeichnet werden müssen, wird ein Buyer mit einem "Schnappschuss" dieser Daten (und möglicherweise anderer Daten) hydratisiert.

buyer.PlaceOrder( cmd.Products );

Als nächstes koordinieren wir den Kauf. Bei der obigen Methode wird ein new Order Erstellt. Ein Order erscheint nicht einfach so aus dem Nichts, oder? Etwas muss es platzieren, also modellieren wir entsprechend. Was bringt das? Nun, die Buyer.PlaceOrder - Methode bietet einen Platz in Ihrer Domain , um BuyerNotInGoodStanding, OrderExceedsBuyerSpendingLimit zu werfen. oder RepeatOrderDetected Ausnahmen. Indem wir nur ein Order im Kontext seiner Platzierung erstellen, können wir erzwingen, wie ein Order entstehen kann. In Ihrem Beispiel müsste entweder Ihr Befehlshandler auf Anwendungsebene oder Ihre Factory-Methode Order für die Durchsetzung jeder Invariante verantwortlich gemacht werden. Weder ist ein guter Ort, um Geschäftsregeln zu überprüfen. Zusätzlich haben wir jetzt einen Ort, an dem wir unser Ereignis OrderPlaced auslösen können (das erforderlich ist, um Ihren Zahlungskontext entkoppelt zu halten), und wir können Ihre Entität Order vereinfachen, da sie jetzt nur noch einen Skalar benötigt buyerId, um den Verweis auf den Eigentümer beizubehalten.

buyers.Save( buyer );

Ziemlich selbsterklärend. Ein Buyer enthält jetzt alle Informationen, die Sie benötigen, um sowohl einen Order als auch einen "Snapshot" von Customer -Daten beizubehalten. Wie Sie diese Daten intern organisieren und für die Persistenz zerlegen, liegt bei Ihnen (Hinweis: Ein Buyer muss beispielsweise überhaupt nicht persistiert werden. Nur das darin enthaltene Order).

[~ # ~] edit [~ # ~]

Die Beispiellösung (wenn wir es so nennen können), die ich gepostet habe, ist eine, die die "Gänge drehen" soll und nicht unbedingt die darstellt bestmögliche Lösung des vorliegenden Problems. Das heißt, Ihr Problem. Es ist durchaus möglich (sogar wahrscheinlich), dass die Einführung des Konzepts eines Buyer - Aggregats überentwickelt ist, da keinerlei Regeln für die Platzierung eines Order erwähnt wurden. Zum Beispiel:

customer = customers.Find( cmd.CustomerId );

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

orders.Save( order );

kann ein völlig gültiger Ansatz sein! Stellen Sie nur sicher, dass alle erforderlichen Informationen in dem CustomerInformationSlip (Ihrem "Snapshot") enthalten sind, der dem Order beigefügt ist, damit er jede invariante Steuerung erzwingen kann, wie er geändert werden kann. Zum Beispiel:

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

Die oben genannten können eine OrderExceedsCustomerShippingDistance auslösen, wenn jede Customer ihre eigenen Regeln hat, wie weit Sie sie aufgrund ihrer Kontostufe versenden werden.

Lassen Sie die Regeln das Design bestimmen!

17
king-side-slide

Ist es in Ordnung, Abfragen durchzuführen, um die Daten zum Erstellen der aggregierten Wurzeln zu erhalten, die ich dann übergeben muss?

Sind Sie besorgt über Datenrennen?

Wenn Sie sich keine Sorgen um ein Datenrennen machen, können Sie sicher eine veraltete Kopie der realen Daten verwenden, und CQRS ist es wirklich egal, woher die veralteten Daten stammen.

Wenn andererseits ein Datenrennen ein Problem darstellen würde, müssen Sie wahrscheinlich anhalten und die Dinge überdenken.

In diesem spezifischen Fall ist es wahrscheinlich in Ordnung, eine Abfrage zu verwenden. Ich denke, weil Sie scheinbar nach Kundeninformationen suchen, die Sie nicht kontrollieren - was Sie abfragen, sind Informationen, die von der realen Welt kontrolliert werden. Ihre Daten sind möglicherweise bereits falsch (der Kunde hat den Namen geändert, aber Sie haben noch keine Benachrichtigung erhalten).

1
VoiceOfUnreason