it-swarm.com.de

CQRS: Befehlsrückgabewerte

Es scheint endlose Verwirrung darüber zu geben, ob Befehle Rückgabewerte haben sollen 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 er zeigt ein Diagramm, in dem Befehle tatsächlich Fehler an den Client zurückgeben.

  • In einem Microsoft Press Store-Artikel heißt es: "Der Befehl gibt keine Antwort zurück."

Während die Erfahrung auf dem Schlachtfeld um CQRS herum zunimmt, konsolidieren sich einige Vorgehensweisen und werden zu Best Practices. Teilweise im Gegensatz zu dem, was wir gerade gesagt haben ... Es ist heutzutage eine gängige Ansicht, dass sowohl der Befehlshandler als auch die Anwendung wissen müssen, wie der Transaktionsvorgang verlaufen ist. Ergebnisse müssen bekannt sein ...

Geben Befehlshandler Werte zurück oder nicht?

Die Antwort?

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

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

Akzeptanz (z. B. Validierung)

Der Befehl "Acceptance" bezieht sich hauptsächlich auf die Validierung. Vermutlich müssen Validierungsergebnisse synchron an den Aufrufer übergeben werden, unabhängig davon, ob der Befehl "erfüllung" synchron ist oder sich in einer Warteschlange befindet.

Es sieht jedoch so aus, als würden viele Anwender die Validierung nicht innerhalb des Befehlshandlers einleiten. Wie 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 überprüft den gültigen Status über Datenanmerkungen), oder an (2) einer Architektur ist vorhanden, was davon ausgeht, dass Befehle an einen (nicht aktiven) Bus oder eine Warteschlange gesendet werden. Diese letzteren Asynchronitätsformen bieten im Allgemeinen keine synchrone Validierungssemantik oder -schnittstellen.

Kurz gesagt, viele Designer möchten, dass der Befehlshandler Validierungsergebnisse als (synchronen) Rückgabewert bereitstellt, müssen jedoch die Einschränkungen der von ihnen verwendeten Asynchronitätstools berücksichtigen.

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 überzeichnet".

In Echtzeit scheint ein Rückgabewert am sinnvollsten zu sein. Ausnahmen sollten nicht verwendet werden, um geschäftsbedingte Fehlerergebnisse mitzuteilen. In einem "Warteschlangenkontext" machen Rückgabewerte natürlich keinen Sinn.

Hier kann die ganze Verwirrung vielleicht zusammengefasst werden:

Viele (die meisten?) CQRS-Anwender gehen davon aus, dass sie jetzt oder in Zukunft ein Asynchron-Framework oder eine Asynchron-Plattform (einen Bus oder eine Warteschlange) einbinden und damit angeben, dass Befehlshandler keine Rückgabewerte haben. Einige Anwender 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-Kontext) angenommen wurde, als Jimmy Bogard stellte diese Beispielbefehlsschnittstelle zur Verfügung :

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 der Grund, warum Jimmy der sich sorgfältig die Zeit genommen hat, eine ungültige Rückgabe von einem Befehl zu erstellen nicht darin bestand, dass "Befehlshandler keine Rückgabewerte haben sollten", sondern stattdessen, dass er einfach seine Mediator-Klasse haben wollte um eine konsistente Schnittstelle zu haben:

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 sinnvollen Rückgabewert haben.

Wiederholen und einpacken

Erfasse ich richtig, warum bei diesem Thema Verwirrung herrscht? Fehlt mir etwas?

55
Brent Arias

Wenn Vladik Khononov dem Rat in Komplexität in CQRS angehen folgt, schlägt er vor, dass die Befehlsverarbeitung Informationen zu ihrem Ergebnis zurückgeben kann.

Ohne Verletzung eines [CQRS] -Prinzips kann ein Befehl die folgenden Daten sicher zurückgeben:

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

Diese Informationen verbessern die Benutzerfreundlichkeit Ihres Systems erheblich, da:

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

Daniel Whittaker befürwortet die Rückgabe eines " common result " - Objekts von einem Befehlshandler, der diese Informationen enthält.

16
Ben Smith

Geben Befehlshandler Werte zurück oder nicht?

Sie sollten keine Geschäftsdaten zurückgeben , sondern nur Metadaten (in Bezug auf Erfolg oder Fehler bei der Ausführung des Befehls). CQRS wird CQS auf eine höhere Ebene 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 eines application service, der das aggregate lädt, dann eine Methode für das aggregate aufruft und dann das aggregate beibehält. Die Absicht des Befehlshandlers besteht darin, das aggregate zu ändern. Sie würden nicht wissen, was Sie zurückgeben sollen, das wäre unabhängig vom Anrufer. Jeder Aufrufer/Client eines Befehlshandlers möchte etwas anderes über den neuen Status erfahren.

Wenn die Befehlsausführung blockiert (auch als synchron bezeichnet), müssen Sie lediglich wissen, ob der Befehl erfolgreich ausgeführt wurde oder nicht. Auf 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.

Denken Sie anders, wenn Sie etwas von einem Befehlshandler zurückgeben, geben Sie ihm zwei Verantwortlichkeiten: 1. Ändern Sie den Aggregatzustand und 2. Fragen Sie ein Lesemodell ab.

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

  1. befehlsprüfung, die bestätigt, dass ein Befehl die richtigen Daten enthält (d. h. 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 prüfen, ob das Aggregat innerhalb des Aggregats mutieren kann, nachdem der Befehl das Aggregat erreicht hat (nachdem eine Methode für das Aggregat aufgerufen wurde).

Wenn wir jedoch eine Stufe nach oben gehen, wird in der Presentation layer (d. h. ein REST -Endpunkt), der Client des Application layer, wir könnten alles zurückgeben und wir werden die Regeln nicht brechen, da die Endpunkte nach den Anwendungsfällen entworfen wurden. Sie wissen in jedem Anwendungsfall genau, was Sie zurückgeben möchten, nachdem ein Befehl ausgeführt wurde.

5

Antwort für @Constantin Galbenu, ich sah mich einer Grenze gegenüber.

@Misanthrope Und was genau machst du mit diesen Ereignissen?

@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 über diese API-Anforderung benachrichtigen.

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

  1. Sie müssen Fehler nicht über Ausnahmen, sondern über Ereignisse melden. Dies passiert normalerweise, wenn Ihr Modell gespeichert werden muss (z. B. wenn die Anzahl der Versuche mit falschem Code/Passwort gezählt wird), auch wenn ein Fehler aufgetreten ist. Außerdem verwenden einige Typen ü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 der Annahme, dass das Auslösen von Geschäftsausnahmen vom Befehlshandler in Ordnung ist, das Zurückgeben von Domänenereignissen jedoch nicht .
  2. Wenn das Ereignis nur unter bestimmten Umständen in Ihrem Gesamtstamm 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 jemanden mögen, der uns schon einmal gemocht hat. Wir müssen den mobilen Client als Antwort darauf benachrichtigen, ob eine Übereinstimmung stattgefunden hat oder nicht. Wenn Sie matchQueryService nur nach dem Befehl überprüfen möchten, finden Sie 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 nicht besiedelten Gebieten, möglicherweise Inkonsistenzen, wahrscheinlich nur bei Ihnen) 2. Gerät usw.).

Es ist so einfach zu überprüfen, ob StrangersWereMatched wirklich gerade passiert ist:

$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 eingeben und das Match-Lesemodell so einrichten, 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. Geringere Wahrscheinlichkeit, einen Fehler zu finden (z. B. wenn matchQueryService zwischengespeichert/verzögert wird [nicht sofort konsistent], liegt ein Problem vor).

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.

1
Misanthrope

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

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

Beachten Sie folgendes Fowlers Zitat :

Die Änderung, die CQRS einführt, besteht darin, dieses konzeptionelle Modell für die Aktualisierung und Anzeige in separate Modelle aufzuteilen, die entsprechend dem Vokabular von CommandQuerySeparation als Command und Query bezeichnet werden.

Es geht um Modelle , nicht Methoden .

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

Darüber hinaus gibt der Autor des Begriffs, Greg Young, an, 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.

1
Misanthrope