Implementieren des Befehlsmusters in einer RESTful-API

12

Ich bin dabei, eine HTTP-API zu entwerfen, die hoffentlich so restvoll wie möglich ist.

Es gibt einige Aktionen, die sich auf einige Ressourcen erstrecken und manchmal rückgängig gemacht werden müssen.

Ich dachte mir, das klingt wie ein Befehlsmuster, aber wie kann ich es in eine Ressource umwandeln?

Ich werde eine neue Ressource namens XXAction vorstellen, wie DepositAction, die durch etwas wie dieses erstellt wird

POST /card/{card-id}/account/{account-id}/Deposit
AmountToDeposit=100, different parameters...

Dadurch wird eine neue DepositAction erstellt und die Do / Execute-Methode aktiviert. In diesem Fall bedeutet die Rückgabe des HTTP-Status 201 Created, dass die Aktion erfolgreich ausgeführt wurde.

Wenn ein Kunde später die Aktionsdetails einsehen möchte, kann er dies

GET /action/{action-id}

Update / PUT sollte wohl gesperrt sein, da es hier nicht relevant ist.

Und um die Aktion rückgängig zu machen, dachte ich über die Verwendung von

DELETE /action/{action-id}

Dadurch wird die Undo-Methode des betreffenden Objekts aufgerufen und dessen Status geändert.

Angenommen, ich bin mit nur einem Do-Undo zufrieden. Ich muss nicht wiederholen.

Ist dieser Ansatz in Ordnung?

Gibt es irgendwelche Tücken, Gründe, es nicht zu benutzen?

Wird dies aus der Sicht der Kunden verstanden?

Mithir
quelle
Kurze Antwort, das ist kein REST.
Evan Plaice
3
@EvanPlaice darauf eingehen? das ist genau die frage.
Mithir
1
Ich hätte eine Antwort ausgearbeitet, aber Garys Antwort deckt bereits fast alles ab, was ich hinzufügen würde. Ich sage, es ist keine Pause, weil URIs nur Ressourcen darstellen sollen (dh keine Aktionen). Aktionen werden über GET / POST / PUT / DELETE / HEAD abgewickelt. Stellen Sie sich REST als OOP-Schnittstelle vor. Ziel ist es, die API an das allgemeine Muster anzupassen und von implementierungsspezifischen Details so weit wie möglich zu entkoppeln.
Evan Plaice
1
@EvanPlaice Ok, ich verstehe, danke. Ich denke, es ist verwirrend hier, weil Deposit als Substantiv und als Verb gedacht werden könnte ...
Mithir
In diesem Fall sollte der URI eine Transaktion darstellen, bei der Abbuchung (Entgegennehmen von Geld) und Gutschrift (Geben von Geld) Aktionen sind, die über POST-Anforderungen ausgeführt werden. POST wird für beide verwendet, da jedes Mal, wenn Geld in eine der beiden Richtungen bewegt wird, eine neue Transaktion erstellt wird. In Ihrem speziellen Fall finden die Transaktionen auf dem Konto eines Karteninhabers statt, sodass die Kontonummer der Karte die Ressourcen-URI ist.
Evan Plaice

Antworten:

13

Sie fügen eine verwirrende Abstraktionsebene hinzu

Ihre API startet sehr sauber und einfach. Ein HTTP-POST erstellt eine neue Einzahlungsressource mit den angegebenen Parametern. Dann gehen Sie von der Strecke, indem Sie die Idee von "Aktionen" einführen, die eher ein Implementierungsdetail als einen Kernbestandteil der API darstellen.

Alternativ können Sie diese HTTP-Konversation in Betracht ziehen ...

POST / card / {Karten-ID} / account / {Konto-ID} / Deposit

AmountToDeposit = 100, verschiedene Parameter ...

201 ERSTELLT

Ort = / card / 123 / account / 456 / Deposit / 789

Jetzt möchten Sie diesen Vorgang rückgängig machen (technisch sollte dies in einem ausgeglichenen Buchhaltungssystem nicht zulässig sein, aber was soll's?):

LÖSCHEN / Karte / 123 / Konto / 456 / Einzahlung / 789

204 KEIN INHALT

Der API-Konsument weiß, dass es sich um eine Einzahlungsressource handelt, und kann bestimmen, welche Vorgänge für diese Ressource zulässig sind (normalerweise über OPTIONS in HTTP).

Obwohl die Implementierung des Löschvorgangs heute über "Aktionen" erfolgt, gibt es keine Garantie dafür, dass das sekundäre Konzept einer "Aktion" beim Migrieren dieses Systems von beispielsweise C # nach Haskell und beim Beibehalten des Frontends weiterhin einen Mehrwert schafft , während das primäre Konzept der Einzahlung sicherlich tut.

Bearbeiten, um eine Alternative zu LÖSCHEN und einzahlen abzudecken

Um einen Löschvorgang zu vermeiden, die Einzahlung jedoch effektiv zu entfernen, sollten Sie folgende Schritte ausführen (mit einer generischen Transaktion, um Ein- und Auszahlungen zu ermöglichen):

POST / card / {Karten-ID} / account / {Konto-ID} / Transaktion

Menge = -100 , verschiedene Parameter ...

201 ERSTELLT

Ort = / card / 123 / account / 456 / Transation / 790

Es wird eine neue Transaktionsressource mit genau dem entgegengesetzten Betrag (-100) erstellt. Dies hat zur Folge, dass das Konto auf 0 zurückgebucht wird und die ursprüngliche Transaktion annulliert wird.

Sie könnten in Betracht ziehen, einen "Utility" -Endpunkt wie diesen zu erstellen

POST / card / {Karten-ID} / account / {Konto-ID} / Transaction / 789 / Undo <- BAD!

um den gleichen Effekt zu erzielen. Dies unterbricht jedoch die Semantik eines URI als Bezeichner, indem ein Verb eingeführt wird. Sie sollten sich besser an Substantive in Bezeichnern halten und die Operationen auf die HTTP-Verben beschränken. Auf diese Weise können Sie auf einfache Weise einen Permalink aus dem Bezeichner erstellen und für GETs usw. verwenden.

Gary Rowe
quelle
3
+1 "Technisch gesehen sollte dies in einem ausgeglichenen Rechnungsführungssystem nicht zulässig sein." Jemand weiß, wie man Bohnen zählt. Diese Aussage ist absolut richtig. Umgekehrt müsste eine weitere Transaktion durchgeführt werden, bei der das Geld zurückerstattet wird. Hauptbucheinträge sollten immer als unveränderlich und dauerhaft betrachtet werden, sobald eine Transaktion abgeschlossen ist.
Evan Plaice
Also, wenn ich in meinen Fragen ändere, anstatt Löschen / Aktion / ... zu Löschen / Einzahlung / ... ist es in Ordnung?
Mithir
2
@Mithir Ich habe die Abrechnungsregel beschrieben. In einem standardmäßigen doppelten Buchhaltungssystem entfernen Sie niemals Transaktionen. Eine einmal begangene Geschichte gilt als unveränderlich, um die Menschen ehrlich zu halten. In Ihrem Fall können Sie immer noch eine DELETE-Aktion verwenden, aber im Back-End (ex-Hauptbuch-Datenbanktabelle) fügen Sie eine weitere Transaktion hinzu, die die Gutschrift (dh die Rückgabe) des Geldes an den Benutzer darstellt. Ich bin kein Buchhalter, aber es ist eine der Standardpraktiken, die in einem "Principles of Accounting I" -Kurs gelehrt werden.
Evan Plaice
2
(Forts.) Datenbankprotokolle verwenden Transaktionen auf ähnliche Weise. Aus diesem Grund ist es möglich, einen Datensatz nur mithilfe der Protokolle zu replizieren und / oder neu zu erstellen. Solange die Transaktionen in der chronologischen Reihenfolge wiedergegeben werden, sollte es möglich sein, den Datensatz von einem beliebigen Punkt in seinem Verlauf aus neu zu erstellen. Durch Entfernen der Mutabilität aus der Gleichung wird die Konsistenz sichergestellt.
Evan Plaice
1
Fair genug, benennen Sie es einfach in Transaktion um.
Gary Rowe
1

Der Hauptgrund für die Existenz von REST ist die Ausfallsicherheit gegenüber Netzwerkfehlern. Zu diesem Zweck sollten alle Operationen idempotent sein .

Der grundsätzliche Ansatz erscheint vernünftig, aber die Art und Weise, wie Sie die DepositActionSchöpfung beschreiben, klingt nicht als idempotent, was behoben werden sollte. Indem der Client eine eindeutige ID bereitstellt, mit der doppelte Anforderungen erkannt werden. Also würde sich die Schöpfung ändern zu

PUT /card/{card-id}/account/{account-id}/Deposit/{action-id}
AmountToDeposit=100, different parameters...

Wenn ein anderer PUT zu derselben URL mit demselben Inhalt wie zuvor durchgeführt wird, sollte die Antwort weiterhin lauten, 201 createdwenn der Inhalt identisch ist, und Fehler, wenn der Inhalt unterschiedlich ist. Auf diese Weise kann der Client die Anforderung einfach erneut senden, wenn sie fehlschlägt, da der Client nicht feststellen kann, ob die Anforderung oder die Antwort verloren gegangen ist.

Es ist sinnvoller, PUT zu verwenden, da es nur die Ressource schreibt und idempotent ist, aber die Verwendung von POST würde auch keine Probleme verursachen.

Um die Transaktionsdetails anzuzeigen, verwendet der Client GETdieselbe URL, d. H

GET /card/{card-id}/account/{account-id}/Deposit/{action-id}

und um es rückgängig zu machen, kann es es LÖSCHEN. Aber wenn es tatsächlich irgendetwas mit Geld zu tun hat, wie es das Beispiel vorschlägt, würde ich vorschlagen, es mit hinzugefügten "abgebrochenen" Flags zu setzen, obwohl dies der Verantwortlichkeit wegen eine Spur von erstellten und abgebrochenen Transaktionen bleibt.

Nun müssen Sie eine Methode zum Erstellen der eindeutigen ID auswählen. Sie haben mehrere Möglichkeiten:

  1. Geben Sie das kundenspezifische Präfix früher im Austausch ein, das enthalten sein muss.
  2. Fügen Sie eine spezielle POST-Anforderung hinzu, um eine leere eindeutige ID vom Server zu erhalten. Diese Anforderung muss nicht idempotent sein (und kann es auch nicht), da nicht verwendete IDs keine Probleme verursachen.
  3. Verwenden Sie einfach die UUID. Jeder benutzt sie und niemand scheint Probleme mit den MAC-basierten oder den zufälligen zu haben.
Jan Hudec
quelle
2
Soweit ich weiß, ist POST nicht idempotent. en.wikipedia.org/wiki/POST_(HTTP)#Affecting_server_state
Mithir
@Mithir: POST wird nicht als idempotent angenommen. es kann immer noch sein. Da jedoch alle REST-Operationen idempotent sein sollen, hat POST im Grunde keinen Platz in REST.
Jan Hudec
1
Ich bin verwirrt ... Inhalt, den ich gelesen habe, und die vorhandene Implementierung (ServiceStack, ASP.NET-Web-API) lassen darauf schließen, dass POST einen Platz in REST hat.
Mithir
3
In REST wird die ID-Befugnis der Ressource zugewiesen, nicht dem Protokoll oder seinen Antwortcodes. Daher werden in REST über HTTP die Methoden GET, PUT, DELETE, PATCH usw. als idempotent betrachtet, obwohl ihre Antwortcodes für nachfolgende Aufrufe variieren können. POST ist in dem Sinne idempotent, dass jeder Aufruf eine neue Ressource erzeugt. Siehe Fielding's Die Verwendung von POST ist in Ordnung .
Gary Rowe
1
Operationen, die nicht idempotent sind, sind in Ruhe zulässig. Diese Behauptung ist völlig falsch.
Andy