CQRS: Befehlsrückgabewerte [geschlossen]

75

Es scheint endlose Verwirrung darüber zu geben, ob Befehle Rückgabewerte haben sollten oder nicht. Ich würde gerne wissen, ob die Verwirrung einfach darauf zurückzuführen ist, dass die Teilnehmer ihren Kontext oder ihre Umstände nicht angegeben haben.

Die Verwirrung

Hier sind Beispiele für die Verwirrung ...

  • Udi Dahan sagt, dass Befehle "keine Fehler an den Client zurückgeben", aber im selben Artikel zeigt er ein Diagramm, in dem Befehle tatsächlich Fehler an den Client zurückgeben.

  • In einem Artikel im Microsoft Press Store heißt es: "Der Befehl ... gibt keine Antwort zurück", gibt dann jedoch eine mehrdeutige Warnung:

Da die Erfahrung auf dem Schlachtfeld um CQRS wächst, konsolidieren sich einige Praktiken und werden tendenziell zu Best Practices. Teilweise im Gegensatz zu dem, was wir gerade gesagt haben ... ist es heutzutage eine verbreitete Ansicht, dass sowohl der Befehlshandler als auch die Anwendung wissen müssen, wie die Transaktionsoperation verlaufen ist. Ergebnisse müssen bekannt sein ...

Geben Befehlshandler Werte zurück oder nicht?

Die Antwort?

Ausgehend von Jimmy Bogards " CQRS Myths " denke ich, dass die Antwort (en) auf diese Frage davon abhängt, von welchem ​​programmatischen / kontextuellen "Quadranten" Sie sprechen:

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

Akzeptanz (zB Validierung)

Der Befehl "Akzeptanz" bezieht sich hauptsächlich auf die Validierung. Vermutlich müssen Validierungsergebnisse synchron an den Anrufer weitergegeben werden, unabhängig davon, ob der Befehl "Erfüllung" synchron ist oder sich in der Warteschlange befindet.

Es scheint jedoch, dass viele Praktiker die Validierung nicht über den Befehlshandler initiieren. Soweit ich gesehen habe, liegt dies entweder daran, dass (1) sie bereits eine fantastische Möglichkeit gefunden haben, die Validierung auf Anwendungsebene durchzuführen (dh ein ASP.NET MVC-Controller, der den gültigen Status über Datenanmerkungen überprüft) oder (2) an einer Architektur ist vorhanden, bei dem davon ausgegangen wird, dass Befehle an einen Bus oder eine Warteschlange (außerhalb des Prozesses) gesendet werden. Diese letzteren Formen der Asynchronität bieten im Allgemeinen keine synchrone Validierungssemantik oder -schnittstellen.

Kurz gesagt, viele Designer möchten möglicherweise, dass der Befehlshandler Validierungsergebnisse als (synchronen) Rückgabewert bereitstellt, müssen jedoch mit den Einschränkungen der von ihnen verwendeten Asynchronisierungstools leben.

Erfüllung

In Bezug auf die "Erfüllung" eines Befehls muss der Client, der den Befehl ausgegeben hat, möglicherweise die scope_identity für einen neu erstellten Datensatz oder möglicherweise Fehlerinformationen kennen, z. B. "Konto überzogen".

In einer Echtzeiteinstellung scheint ein Rückgabewert am sinnvollsten zu sein. Ausnahmen sollten nicht verwendet werden, um geschäftsbezogene Fehlerergebnisse zu kommunizieren. In einem "Warteschlangen" -Kontext ... geben Rückgabewerte natürlich keinen Sinn.

Hier lässt sich vielleicht die ganze Verwirrung zusammenfassen:

Viele (die meisten?) CQRS-Anwender gehen davon aus, dass sie jetzt oder in Zukunft ein Asynchronitätsframework oder eine Asynchronitätsplattform (einen Bus oder eine Warteschlange) integrieren werden, und proklamieren daher, dass Befehlshandler keine Rückgabewerte haben. Einige Praktiker haben jedoch nicht die Absicht, solche ereignisgesteuerten Konstrukte zu verwenden, und unterstützen daher Befehlshandler, die (synchron) Werte zurückgeben.

Ich glaube zum Beispiel, dass ein synchroner Kontext (Anfrage-Antwort) angenommen wurde, als Jimmy Bogard diese Beispielbefehlsschnittstelle bereitstellte :

public interface ICommand<out TResult> { }

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

Sein Mediatr-Produkt ist schließlich ein In-Memory-Tool. Angesichts all dessen denke ich, dass Jimmy sich die Zeit sorgfältig genommen hat, um eine ungültige Rückgabe von einem Befehl zu erzeugen, nicht, weil "Befehlshandler keine Rückgabewerte haben sollten", sondern weil er einfach wollte, dass seine Mediator-Klasse eine konsistente Schnittstelle hat:

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

... obwohl nicht alle Befehle einen aussagekräftigen Wert für die Rückgabe haben.

Wiederholen und abschließen

Erfasse ich richtig, warum es zu diesem Thema Verwirrung gibt? Fehlt mir etwas?

Update (6/2020)

Mit Hilfe der gegebenen Antworten denke ich, dass ich die Verwirrung entwirrt habe. Einfach ausgedrückt, wenn ein CQRS-Befehl einen Erfolg / Misserfolg zurückgeben kann, der den Abschlussstatus anzeigt , ist ein Rückgabewert sinnvoll. Dies umfasst die Rückgabe einer neuen DB-Zeilenidentität oder eines Ergebnisses, das den Inhalt des Domänenmodells (Geschäfts) nicht liest oder zurückgibt.

Ich denke, wo "CQRS-Befehl" Verwirrung auftaucht, ist über die Definition und Rolle von "Asynchronität". Es gibt einen großen Unterschied zwischen "aufgabenbasierten" asynchronen E / A-Vorgängen und einer asynchronen Architektur (z. B. warteschlangenbasierte Middleware). Im ersteren Fall kann und wird die asynchrone "Task" das Abschlussergebnis für den asynchronen Befehl liefern. Ein an RabbitMQ gesendeter Befehl erhält jedoch ebenfalls keine Benachrichtigung über den Abschluss der Anforderung / Antwort. Es ist dieser letztere Kontext der Async-Architektur, der einige dazu bringt zu sagen: "Es gibt keinen asynchronen Befehl" oder "Befehle geben keine Werte zurück".

Brent Arien
quelle
Schöne Frage, aber nicht ganz klar. Befehle sind nur VO, keine Methoden, sie geben nichts zurück; wahrscheinlich beziehen Sie sich auf die Behandlung von Befehlen; Bitte geben Sie die Ebene / Ebene an: Präsentation (dh Rest), Anwendung, Geschäftsebene (Aggregate).
Constantin Galbenu

Antworten:

21

Im Anschluss an die Beratung in Angreifen Komplexität in CQRS von Vladik Khononov schlägt Befehl Handhabung Informationen in Bezug auf ihr Ergebnis zurückgeben kann.

Ohne gegen [CQRS] -Prinzipien zu verstoßen, kann ein Befehl die folgenden Daten sicher zurückgeben:

  • Ausführungsergebnis: Erfolg oder Misserfolg;
  • Fehlermeldungen oder Validierungsfehler im Fehlerfall;
  • Die neue Versionsnummer des Aggregats bei Erfolg;

Diese Informationen verbessern die Benutzererfahrung Ihres Systems erheblich, da:

  • Sie müssen keine externe Quelle nach dem Ergebnis der Befehlsausführung abfragen, Sie haben es sofort. Es wird trivial, Befehle zu validieren und Fehlermeldungen zurückzugeben.
  • Wenn Sie die angezeigten Daten aktualisieren möchten, können Sie anhand der neuen Version des Aggregats bestimmen, ob das Ansichtsmodell den ausgeführten Befehl widerspiegelt oder nicht. Keine veralteten Daten mehr anzeigen.

Daniel Whittaker befürwortet die Rückgabe eines Objekts mit " gemeinsamem Ergebnis " von einem Befehlshandler, der diese Informationen enthält.

Ben Smith
quelle
4
Was ist mit dem Befehl, der ein neues Objekt erstellt? Sollte er nicht mindestens die ID des erstellten Objekts zurückgeben?
Greyshack
4
@Greyshack Dies ist vermutlich der Grund, warum die meisten ES / CQRS-Ressourcen die Verwendung von GUIDs für Aggregat- / Entitäts-IDs vorschlagen. Der Client kann die ID generieren und in den Erstellungsbefehl aufnehmen, anstatt sie dem Backend zu überlassen, um eine ID zu generieren.
Sam Jones
9

Geben Befehlshandler Werte zurück oder nicht?

Sie sollten keine Geschäftsdaten zurückgeben , sondern nur Metadaten (in Bezug auf den Erfolg oder Misserfolg der Ausführung des Befehls). CQRSist CQS auf ein höheres Niveau gebracht. Selbst wenn Sie die Regeln des Puristen brechen und etwas zurückgeben würden, was würden Sie zurückgeben? In CQRS ist der Befehlshandler eine Methode von application service, die lädt und aggregatedann eine Methode aufruft, die aggregatedann beibehalten wird aggregate. Der Befehlshandler beabsichtigt, das zu ändern aggregate. Sie würden nicht wissen, was Sie zurückgeben sollen, das wäre unabhängig vom Anrufer. Jeder Befehlshandler-Aufrufer / Client möchte etwas anderes über den neuen Status wissen.

Wenn die Befehlsausführung blockiert (auch bekannt als synchron), müssen Sie nur wissen, ob der Befehl erfolgreich ausgeführt wurde oder nicht. In einer höheren Ebene würden Sie dann genau das abfragen, was Sie über den Status der neuen Anwendung wissen müssen, indem Sie ein Abfragemodell verwenden, das Ihren Anforderungen am besten entspricht.

Wenn Sie etwas von einem Befehlshandler zurückgeben, geben Sie ihm zwei Verantwortlichkeiten: 1. Ändern Sie den Aggregatstatus und 2. fragen Sie ein Lesemodell ab.

In Bezug auf die Befehlsüberprüfung gibt es mindestens zwei Arten der Befehlsüberprüfung:

  1. Befehlssicherheitsprüfung, die überprüft, ob ein Befehl die richtigen Daten enthält (dh eine E-Mail-Adresse ist gültig); Dies erfolgt, bevor der Befehl das Aggregat erreicht, im Befehlshandler (dem Anwendungsdienst) oder im Befehlskonstruktor.
  2. Domäneninvarianten überprüfen, die innerhalb des Aggregats ausgeführt werden, nachdem der Befehl das Aggregat erreicht hat (nachdem eine Methode für das Aggregat aufgerufen wurde) und ob das Aggregat in den neuen Status mutieren kann.

Wenn wir jedoch eine Stufe höher gehen, in dem Presentation layer(dh einem RESTEndpunkt), dem Client von Application layer, könnten wir alles zurückgeben und wir werden die Regeln nicht brechen, da die Endpunkte nach den Anwendungsfällen entworfen wurden, wissen Sie genau, was Sie wollen Rückkehr, nachdem ein Befehl ausgeführt wurde, in jedem Anwendungsfall.

Constantin Galbenu
quelle
2
"Sie wissen in jedem Anwendungsfall genau, was Sie nach Ausführung eines Befehls zurückgeben möchten" - nein, ich nicht. Wenn der Client den Dienst A anfordert, der über CQRS und den internen Befehlshandler verfügt, ordnet er einige Daten neu zu und ruft im Hintergrund den Dienst B auf (im Auftrag eines Clients), der dann die ID der neu erstellten Ressource zurückgibt. Wie würde ich diese ID an den Client zurückgeben, wenn Der Befehl von Service A gibt keinen Wert zurück?
Wirone
2
@Wirone Im Allgemeinen werden in CQRS GUIDs verwendet, sodass Sie die ID der Entität bereits kennen, bevor Sie den Befehl ausführen. Sie kennen also die ID und den Anwendungsfall => Sie kennen das Readmodel
Constantin Galbenu
2
"Sie sollten nicht. CQRS ist CQS auf ein höheres Niveau gebracht." CQRS ist eine übergeordnete Idee und kümmert sich nicht um Methoden. Die Hauptidee in CQRS besteht darin, das Mischen von Schreib- und Lesemodellen zu vermeiden, aber der Befehlshandler kann (MUSS) Fehler und sogar erzeugte Ereignisse zurückgeben.
Misanthrope
2
@Misanthrope Ich stimme zu, bitte lesen Sie meine Antwort noch einmal, insbesondere den letzten Absatz. Der Befehlshandler (nur um sicherzugehen: Die Methode, die das Aggregat lädt, die Befehlsmethode aufruft und dann das Aggregat beibehält) sollte nichts zurückgeben. Wenn Sie Ereignisse benötigen, abonnieren Sie diese und der HTTP-Handler (nicht der Command-Handler!) Kann diese Ereignisse sammeln und zurückgeben. Dies gilt auch für die Ausnahmen. Zum Teufel, der HTTP-Handler (oder was auch immer Sie über dem Command-Handler haben) kann ein Lesemodell oder etwas abfragen und einen Status zurückgeben.
Constantin Galbenu
2
"Wenn Sie Ereignisse benötigen, abonnieren Sie diese." Ich sehe in diesem Fall keinen Grund, dies zu tun. Der Befehlshandler konvertiert im Allgemeinen nur Befehle in Ereignisse. Das Abrufen von Ereignissen ist in diesem Fall nur expliziter.
Misanthrope
4

CQRS und CQS sind wie Mikrodienste und Klassenzerlegung: Die Hauptidee ist dieselbe ("neigen zu kleinen zusammenhängenden Modulen"), aber sie liegen auf verschiedenen semantischen Ebenen.

Der Zweck von CQRS besteht darin, Schreib- / Lesemodelle zu trennen. Solche Details auf niedriger Ebene wie der Rückgabewert einer bestimmten Methode sind völlig irrelevant.

Beachten Sie das folgende Zitat von Fowler :

Die von CQRS eingeführte Änderung besteht darin, dieses konzeptionelle Modell zur Aktualisierung und Anzeige in separate Modelle aufzuteilen, die nach dem Vokabular von CommandQuerySeparation als Befehl bzw. Abfrage bezeichnet werden.

Es geht um Modelle , nicht um Methoden .

Der Befehlshandler kann alles außer Lesemodellen zurückgeben: Status (Erfolg / Misserfolg), generierte Ereignisse (das Hauptziel der Befehlshandler, übrigens: Ereignisse für den angegebenen Befehl zu generieren), Fehler. Befehlshandler lösen sehr oft eine ungeprüfte Ausnahme aus. Dies ist ein Beispiel für Ausgangssignale von Befehlshandlern.

Darüber hinaus sagt der Autor des Begriffs, Greg Young, dass Befehle immer synchron sind (andernfalls werden sie zu Ereignissen): https://groups.google.com/forum/#!topic/dddcqrs/xhJHVxDx2pM

Greg Young

Eigentlich habe ich gesagt, dass es keinen asynchronen Befehl gibt :) Es ist eigentlich ein anderes Ereignis.

Menschenfeind
quelle
2

Antwort für @Constantin Galbenu, ich stand vor Limit.

@ Misanthrope Und was genau machst du mit diesen Events?

@Constantin Galbenu, in den meisten Fällen brauche ich sie natürlich nicht als Ergebnis des Befehls. In einigen Fällen muss ich den Client als Antwort auf diese API-Anfrage benachrichtigen.

Es ist äußerst nützlich, wenn:

  1. Sie müssen über Ereignisse anstelle von Ausnahmen über Fehler informieren. Dies geschieht normalerweise, wenn Ihr Modell gespeichert werden muss (z. B. zählt die Anzahl der Versuche mit falschem Code / Passwort), auch wenn ein Fehler aufgetreten ist. Außerdem verwenden einige Leute überhaupt keine Ausnahmen für Geschäftsfehler - nur Ereignisse ( http://andrzejonsoftware.blogspot.com/2014/06/custom-exceptions-or-domain-events.html ) Es gibt keinen besonderen Grund zu denken, dass das Auslösen von Geschäftsausnahmen aus dem Befehlshandler in Ordnung ist, das Zurückgeben von Domänenereignissen jedoch nicht .
  2. Wenn ein Ereignis nur unter bestimmten Umständen innerhalb Ihres aggregierten Stamms auftritt.

Und ich kann ein Beispiel für den zweiten Fall geben. Stellen Sie sich vor, wir machen einen Zunder-ähnlichen Dienst, wir haben den LikeStranger-Befehl. Dieser Befehl kann zu StrangersWereMatched führen, wenn wir eine Person mögen, die uns bereits zuvor gemocht hat. Wir müssen den mobilen Client darüber informieren, ob eine Übereinstimmung stattgefunden hat oder nicht. Wenn Sie nach dem Befehl nur matchQueryService überprüfen möchten, finden Sie dort möglicherweise eine Übereinstimmung, aber es gibt keine Garantie dafür, dass die Übereinstimmung gerade stattgefunden hat, da SOMETIMES Tinder bereits übereinstimmende Fremde anzeigt (wahrscheinlich in unbewohnten Bereichen, möglicherweise inkonsistent, wahrscheinlich nur 2. Gerät usw.).

Das Überprüfen der Antwort, ob StrangersWereMatched gerade wirklich passiert ist, ist so einfach:

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

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

Ja, Sie können beispielsweise die Befehls-ID einführen und das Match-Lesemodell so einstellen, dass es erhalten bleibt:

// ...

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

... aber denken Sie darüber nach: Warum ist das erste Beispiel mit einfacher Logik Ihrer Meinung nach schlechter? Es verstößt sowieso nicht gegen CQRS, ich habe nur das Implizite explizit gemacht. Es ist ein staatenloser unveränderlicher Ansatz. Weniger Chancen, einen Fehler zu finden (z. B. wenn matchQueryServicezwischengespeichert / verzögert wird [nicht sofort konsistent], haben Sie ein Problem).

Ja, wenn die Tatsache des Abgleichs nicht ausreicht und Sie Daten für die Antwort abrufen müssen, müssen Sie den Abfragedienst verwenden. Nichts hindert Sie jedoch daran, Ereignisse vom Befehlshandler zu empfangen.

Menschenfeind
quelle