Konzeptionelle Inkongruenz zwischen DDD Application Services und REST-API

20

Ich versuche, eine Anwendung zu entwerfen, die eine komplexe Geschäftsdomäne aufweist und eine REST-API unterstützen muss (nicht ausschließlich REST, sondern ressourcenorientiert). Ich habe einige Probleme damit, das Domänenmodell ressourcenorientiert darzustellen.

In DDD müssen Clients eines Domänenmodells die prozedurale Ebene "Application Services" durchlaufen, um auf alle Geschäftsfunktionen zuzugreifen, die von Entities und Domain Services implementiert werden. Beispielsweise gibt es einen Anwendungsdienst mit zwei Methoden zum Aktualisieren einer Benutzerentität:

userService.ChangeName(name);
userService.ChangeEmail(email);

Die API dieses Anwendungsdienstes macht Befehle (Verben, Prozeduren) verfügbar, nicht den Status.

Wenn wir jedoch auch eine RESTful-API für dieselbe Anwendung bereitstellen müssen, gibt es ein Benutzerressourcenmodell, das wie folgt aussieht:

{
name:"name",
email:"[email protected]"
}

Die ressourcenorientierte API macht den Status und keine Befehle verfügbar . Dies wirft folgende Bedenken auf:

  • Jeder Aktualisierungsvorgang für eine REST-API kann einem oder mehreren Application Service-Prozeduraufrufen zugeordnet werden, je nachdem, welche Eigenschaften im Ressourcenmodell aktualisiert werden

  • Jeder Aktualisierungsvorgang sieht für den REST-API-Client atomar aus, ist jedoch nicht so implementiert. Jeder Aufruf von Application Service ist als separate Transaktion konzipiert. Das Aktualisieren eines Felds in einem Ressourcenmodell kann die Validierungsregeln für andere Felder ändern. Wir müssen daher alle Ressourcenmodellfelder gemeinsam validieren, um sicherzustellen, dass alle potenziellen Application Service-Aufrufe gültig sind, bevor wir mit deren Erstellung beginnen. Das gleichzeitige Überprüfen einer Reihe von Befehlen ist weitaus weniger trivial als das gleichzeitige Ausführen von Befehlen. Wie machen wir das auf einem Client, der nicht einmal weiß, dass einzelne Befehle existieren?

  • Das Aufrufen von Application Service-Methoden in unterschiedlicher Reihenfolge hat möglicherweise einen anderen Effekt, während die REST-API den Eindruck erweckt, dass es keinen Unterschied gibt (innerhalb einer Ressource).

Ich könnte mir ähnliche Probleme ausdenken, aber im Grunde genommen werden sie alle durch dasselbe verursacht. Nach jedem Aufruf eines Anwendungsdienstes ändert sich der Status des Systems. Regeln für die gültige Änderung, die Menge der Aktionen, die eine Entität bei der nächsten Änderung ausführen kann. Eine ressourcenorientierte API versucht, alles wie eine atomare Operation aussehen zu lassen. Aber die Komplexität, diese Lücke zu überwinden, muss irgendwohin gehen, und es scheint gewaltig.

Wenn die Benutzeroberfläche mehr befehlsorientiert ist, was häufig der Fall ist, müssen wir außerdem auf der Clientseite eine Zuordnung zwischen Befehlen und Ressourcen vornehmen und dann wieder auf der API-Seite.

Fragen:

  1. Sollte all diese Komplexität nur von einer (dicken) REST-zu-AppService-Zuordnungsebene bewältigt werden?
  2. Oder vermisse ich etwas in meinem Verständnis von DDD / REST?
  3. Könnte REST einfach nicht praktikabel sein, um die Funktionalität von Domänenmodellen über einen bestimmten (relativ geringen) Komplexitätsgrad hinweg verfügbar zu machen?
Astreltsov
quelle
Stellen Sie sich den REST-Client als Benutzer des Systems vor. Es ist ihnen absolut egal, WIE das System die von ihm ausgeführten Aktionen ausführt. Sie würden nicht mehr erwarten, dass der REST-Client alle verschiedenen Aktionen in der Domäne kennt, als Sie es von einem Benutzer erwarten würden. Wie Sie sagen, muss diese Logik irgendwo hin, aber in jedem System muss sie irgendwo hin, wenn Sie REST nicht verwenden, verschieben Sie sie einfach in den Client. Wenn Sie dies nicht tun, ist dies genau der Punkt von REST. Der Client sollte nur wissen, dass er den Status aktualisieren möchte, und sollte keine Ahnung haben, wie Sie dies tun.
Cormac Mulhall
2
@astr Die einfache Antwort lautet, dass Ressourcen nicht Ihr Modell sind. Der Entwurf von Ressourcenbehandlungscode sollte sich also nicht auf den Entwurf Ihres Modells auswirken. Ressourcen sind ein nach außen gerichteter Aspekt des Systems, da das Modell intern ist. Stellen Sie sich Ressourcen genauso vor wie die Benutzeroberfläche. Ein Benutzer klickt möglicherweise auf eine einzelne Schaltfläche in der Benutzeroberfläche, und im Modell werden hundert verschiedene Vorgänge ausgeführt. Ähnlich einer Ressource. Ein Client aktualisiert eine Ressource (eine einzelne PUT-Anweisung), und im Modell können eine Million verschiedene Vorgänge ausgeführt werden. Es ist ein Anti-Pattern, Ihr Modell eng an Ihre Ressourcen zu koppeln.
Cormac Mulhall
1
Dies ist ein gutes Gespräch darüber, wie Sie Aktionen in Ihrer Domain als Nebeneffekte von REST- Statusänderungen
Cormac Mulhall
1
Ich bin mir auch nicht sicher, was den ganzen "Benutzer als Roboter / Zustandsmaschine" angeht. Ich denke, wir sollten uns bemühen, unsere Benutzeroberflächen viel natürlicher zu gestalten ...
guillaume31

Antworten:

10

Ich hatte das gleiche Problem und habe es "gelöst", indem ich REST-Ressourcen unterschiedlich modelliert habe, zB:

/users/1  (contains basic user attributes) 
/users/1/email 
/users/1/activation 
/users/1/address

Deshalb habe ich die größere, komplexe Ressource in mehrere kleinere aufgeteilt. Jede von diesen enthält eine etwas zusammenhängende Gruppe von Attributen der ursprünglichen Ressource, von denen erwartet wird, dass sie zusammen verarbeitet werden.

Jede Operation auf diesen Ressourcen ist atomar, auch wenn sie mit mehreren Dienstmethoden implementiert werden kann - zumindest in Spring / Java EE ist es kein Problem, eine größere Transaktion aus mehreren Methoden zu erstellen, für die ursprünglich eine eigene Transaktion vorgesehen war (mit REQUIRED-Transaktion) Vermehrung). Sie müssen für diese spezielle Ressource häufig noch eine zusätzliche Validierung durchführen, diese ist jedoch immer noch überschaubar, da die Attribute zusammenhängend sind (sein sollen).

Dies ist auch für den HATEOAS-Ansatz von Vorteil, da Ihre feinkörnigeren Ressourcen mehr Informationen darüber liefern, was Sie mit ihnen tun können (anstatt diese Logik auf Client und Server zu haben, da sie nicht einfach in Ressourcen dargestellt werden kann).

Es ist natürlich nicht perfekt - wenn Benutzeroberflächen nicht unter Berücksichtigung dieser Ressourcen modelliert werden (insbesondere datenorientierte Benutzeroberflächen), kann dies zu Problemen führen - z. B. zeigt die Benutzeroberfläche eine große Form aller Attribute bestimmter Ressourcen (und ihrer Subressourcen) und ermöglicht dies Bearbeiten Sie sie alle und speichern Sie sie auf einmal - dies erzeugt die Illusion von Atomizität, obwohl der Client mehrere Ressourcenoperationen aufrufen muss (die selbst atomar sind, die gesamte Sequenz jedoch nicht atomar).

Auch diese Aufteilung der Ressourcen ist manchmal nicht einfach oder offensichtlich. Ich mache dies hauptsächlich an Ressourcen mit komplexen Verhaltensweisen / Lebenszyklen, um deren Komplexität zu managen.

qbd
quelle
Das habe ich mir auch überlegt: Erstellen Sie detailliertere Ressourcendarstellungen, weil sie für Schreibvorgänge bequemer sind. Wie gehen Sie mit der Abfrage von Ressourcen um, wenn diese so detailliert sind? Auch schreibgeschützte de-normalisierte Darstellungen erstellen?
Astreltsov
1
Nein, ich habe keine schreibgeschützten de-normalisierten Darstellungen. Ich verwende den jsonapi.org- Standard und er verfügt über einen Mechanismus, um verwandte Ressourcen in die Antwort für eine bestimmte Ressource aufzunehmen. Grundsätzlich sage ich "gib mir User mit ID 1 und inkludiere auch deren Subressourcen Email und Freischaltung". Dies hilft dabei, zusätzliche REST-Aufrufe für Subressourcen zu beseitigen, und hat keinen Einfluss auf die Komplexität des Clients, der mit den Subressourcen umgeht, wenn Sie eine gute JSON-API-Clientbibliothek verwenden.
qbd
Eine einzelne GET-Anforderung auf dem Server führt also zu einer oder mehreren tatsächlichen Abfragen (je nachdem, wie viele Subressourcen enthalten sind), die dann zu einem einzelnen Ressourcenobjekt zusammengefasst werden.
Astreltsov
Was ist, wenn mehr als eine Verschachtelungsebene erforderlich ist?
Astreltsov
Ja, in relationalen Datenbanken wird dies wahrscheinlich zu mehreren Abfragen führen. Die willkürliche Verschachtelung wird von der JSON-API unterstützt. Sie wird hier beschrieben: jsonapi.org/format/#fetching-includes
qbd
0

Das zentrale Problem hierbei ist, wie die Geschäftslogik transparent aufgerufen wird, wenn ein REST-Aufruf erfolgt. Dies ist ein Problem, das von REST nicht direkt behoben wird.

Ich habe dieses Problem gelöst, indem ich über einen Persistenzanbieter wie JPA eine eigene Datenverwaltungsebene erstellt habe. Mithilfe eines Metamodells mit benutzerdefinierten Anmerkungen können wir die entsprechende Geschäftslogik aufrufen, wenn sich der Entitätsstatus ändert. Dadurch wird sichergestellt, dass unabhängig davon, wie sich der Entitätsstatus ändert, die Geschäftslogik aufgerufen wird. Es hält Ihre Architektur und auch Ihre Geschäftslogik an einem Ort.

Mit dem obigen Beispiel können wir eine Geschäftslogikmethode namens validateName aufrufen, wenn das Namensfeld mit REST geändert wird:

class User { 
      String name;
      String email;

      /**
       * This method will be transparently invoked when the value of name is changed
       * by REST.
       * The XorUpdate annotation becomes effective for PUT/POST actions
       */
      @XorPostChange
      public void validateName() {
        if(name == null) {
          throw new IllegalStateException("Name cannot be set as null");
        }
      }
    }

Mit einem solchen Tool müssen Sie lediglich Ihre Geschäftslogikmethoden entsprechend mit Anmerkungen versehen.

codedabbler
quelle
0

Ich habe einige Probleme damit, das Domänenmodell ressourcenorientiert darzustellen.

Sie sollten das Domänenmodell nicht ressourcenorientiert verfügbar machen. Sie sollten die Anwendung ressourcenorientiert verfügbar machen.

Wenn die Benutzeroberfläche befehlsorientierter ist, was häufig der Fall ist, müssen wir zwischen Befehlen und Ressourcen auf der Clientseite und dann wieder auf der API-Seite abbilden.

Überhaupt nicht - senden Sie die Befehle an Anwendungsressourcen, die mit dem Domänenmodell verbunden sind.

Jeder Aktualisierungsvorgang für eine REST-API kann einem oder mehreren Application Service-Prozeduraufrufen zugeordnet werden, je nachdem, welche Eigenschaften im Ressourcenmodell aktualisiert werden

Ja, obwohl es einen etwas anderen Weg gibt, dies zu buchstabieren, der die Dinge einfacher machen könnte; Jede Aktualisierungsoperation für eine REST-API ist einem Prozess zugeordnet, der Befehle an ein oder mehrere Aggregate sendet.

Jeder Aktualisierungsvorgang sieht für den REST-API-Client atomar aus, ist jedoch nicht so implementiert. Jeder Aufruf von Application Service ist als separate Transaktion konzipiert. Das Aktualisieren eines Felds in einem Ressourcenmodell kann die Validierungsregeln für andere Felder ändern. Wir müssen daher alle Ressourcenmodellfelder gemeinsam validieren, um sicherzustellen, dass alle potenziellen Application Service-Aufrufe gültig sind, bevor wir mit deren Erstellung beginnen. Das gleichzeitige Überprüfen einer Reihe von Befehlen ist weitaus weniger trivial als das gleichzeitige Ausführen von Befehlen. Wie machen wir das auf einem Client, der nicht einmal weiß, dass einzelne Befehle existieren?

Sie jagen hier den falschen Schwanz.

Stellen Sie sich vor: Nehmen Sie REST vollständig aus dem Bild. Stellen Sie sich stattdessen vor, Sie schreiben eine Desktop-Oberfläche für diese Anwendung. Stellen wir uns weiter vor, dass Sie wirklich gute Designanforderungen haben und eine aufgabenbasierte Benutzeroberfläche implementieren. So erhält der Benutzer eine minimalistische Oberfläche, die perfekt auf die Aufgabe abgestimmt ist, die er gerade bearbeitet. Der Benutzer gibt einige Eingaben ein und drückt dann auf "VERB!" Taste.

Was passiert jetzt? Aus Sicht des Benutzers ist dies eine einzelne atomare Aufgabe. Aus der Sicht des domainModel handelt es sich um eine Reihe von Befehlen, die von Aggregaten ausgeführt werden, wobei jeder Befehl in einer separaten Transaktion ausgeführt wird. Die sind völlig inkompatibel! Wir brauchen etwas in der Mitte, um die Lücke zu schließen!

Das Etwas ist "die Anwendung".

Auf dem Happy-Pfad empfängt die Anwendung einige DTOs und analysiert dieses Objekt, um eine Nachricht zu erhalten, die sie versteht, und verwendet die Daten in der Nachricht, um wohlgeformte Befehle für ein oder mehrere Aggregate zu erstellen. Die Anwendung stellt sicher, dass alle Befehle, die an die Aggregate gesendet werden, korrekt sind (dies ist die Antikorruptionsschicht bei der Arbeit), lädt die Aggregate und speichert die Aggregate, wenn die Transaktion erfolgreich abgeschlossen wurde. Das Aggregat entscheidet für sich, ob der Befehl in Anbetracht seines aktuellen Status gültig ist.

Mögliche Ergebnisse: Die Befehle werden alle erfolgreich ausgeführt. Die Anti-Korruptions-Ebene weist die Meldung zurück. Einige der Befehle werden erfolgreich ausgeführt. Dann beklagt sich eines der Aggregate und es besteht die Möglichkeit, dass Sie Abhilfemaßnahmen ergreifen.

Stellen Sie sich vor, Sie haben diese Anwendung erstellt. Wie interagiert man auf RESTvolle Weise damit?

  1. Der Client beginnt mit einer Hypermedia-Beschreibung seines aktuellen Status (dh der aufgabenbasierten Benutzeroberfläche), einschließlich der Hypermedia-Steuerelemente.
  2. Der Client sendet eine Darstellung der Aufgabe (dh das DTO) an die Ressource.
  3. Die Ressource analysiert die eingehende HTTP-Anforderung, erfasst die Darstellung und übergibt sie an die Anwendung.
  4. Die Anwendung führt die Aufgabe aus. Aus Sicht der Ressource ist dies eine Black Box mit einem der folgenden Ergebnisse
    • Die Anwendung hat alle Aggregate erfolgreich aktualisiert: Die Ressource meldet dem Client den Erfolg und leitet ihn an einen neuen Anwendungsstatus weiter
    • Die Antikorruptionsschicht weist die Meldung zurück: Die Ressource meldet dem Client einen 4xx-Fehler (wahrscheinlich eine ungültige Anforderung) und gibt möglicherweise eine Beschreibung des aufgetretenen Problems weiter.
    • Die Anwendung aktualisiert einige Aggregate: Die Ressource meldet dem Client, dass der Befehl akzeptiert wurde, und leitet den Client an eine Ressource weiter, die den Fortschritt des Befehls darstellt.

Accepted (Akzeptiert) ist das übliche Cop-out, wenn die Anwendung die Verarbeitung einer Nachricht bis nach der Antwort an den Client zurückstellt. Dies wird normalerweise beim Akzeptieren eines asynchronen Befehls verwendet. Aber es funktioniert auch gut für diesen Fall, in dem eine Operation, die atomar sein soll, eine Minderung erfordert.

In dieser Redewendung stellt die Ressource die Aufgabe selbst dar. Sie starten eine neue Instanz der Aufgabe, indem Sie die entsprechende Darstellung an die Aufgabenressource senden. Diese Ressource ist mit der Anwendung verbunden und leitet Sie zum nächsten Anwendungsstatus weiter.

In möchten Sie so ziemlich jedes Mal, wenn Sie mehrere Befehle koordinieren, in einem Prozess (auch bekannt als Geschäftsprozess oder Saga) denken.

Es gibt eine ähnliche konzeptionelle Abweichung im Lesemodell. Betrachten Sie erneut die aufgabenbasierte Schnittstelle. Wenn für die Aufgabe mehrere Aggregate geändert werden müssen, enthält die Benutzeroberfläche zur Vorbereitung der Aufgabe wahrscheinlich Daten aus einer Reihe von Aggregaten. Wenn Ihr Ressourcenschema 1: 1 mit Aggregaten ist, wird es schwierig sein, dies zu arrangieren. Stellen Sie stattdessen eine Ressource bereit, die eine Darstellung der Daten aus mehreren Aggregaten zusammen mit einem Hypermedien-Steuerelement zurückgibt, das die Beziehung "Startaufgabe" zum Aufgabenendpunkt wie oben beschrieben abbildet.

Siehe auch: REST in Practice von Jim Webber.

VoiceOfUnreason
quelle
Wenn wir die API so gestalten, dass sie gemäß unseren Anwendungsfällen mit unserer Domain interagiert. Warum sollten wir die Dinge nicht so gestalten, dass Sagas überhaupt nicht erforderlich sind? Vielleicht fehlt mir etwas, aber wenn ich Ihre Antwort lese, glaube ich wirklich, dass REST nicht gut zu DDD passt und es besser ist, Remoteprozeduren (RPC) zu verwenden. DDD ist verhaltensorientiert, während REST http-verbzentriert ist. Warum nicht REST aus dem Bild entfernen und das Verhalten (Befehle) in der API offenlegen? Schließlich wurden sie wahrscheinlich entwickelt, um Anwendungsszenarien zu erfüllen, und es handelt sich wahrscheinlich um transaktionale Szenarien. Was ist der Vorteil von REST, wenn wir die Benutzeroberfläche besitzen?
iberodev