Entwerfen einer funktionsbasierten RESTful-API

8

Bitte führen Sie einen Streit zwischen mir und einem Freund.

Wir entwickeln derzeit eine Produkt-API. Unsere Produkteinheit sieht so aus

{
    "Id": "",
    "ProductName": "",
    "StockQuantity": 0
}

Produktverkäufe werden von Dritten abgewickelt und sind verpflichtet, uns die gekaufte Menge mitzuteilen, damit das StockQuantityFeld verkleinert werden kann.

Mein Ansatz:

PUT /api/Product/{Id}/ --data { "StockQuantity": "{NewStockQuantity}" }

Der Dritte ist dafür verantwortlich, das Produkt abzufragen, die Berechnung auf der Grundlage der aktuellen StockQuantityund gekauften Menge durchzuführen und eine PUTAnfrage mit dem neuen Wert zu senden .

Mein Freund möchte nicht, dass der Dritte die Berechnung durchführt. Sein Ansatz

PUT /api/Product/{Id}/DecreaseStock --data { "PurchasedQuantity": "{PurchasedQuantity}" }

So können wir die Berechnung durchführen und die aktualisieren StockQuantity

Ich möchte keine funktionsbasierten Endpunkte erstellen und er möchte nicht auf Dritte vertrauen, um die Berechnungen durchzuführen.

Was wäre der richtige Weg für uns, um dieses Problem anzugehen?

Sefa Ümit Oray
quelle
Denken Sie daran, dass PUT (theoretisch) idempotent sein sollte. Option 1 würde in die Semantik passen. Option 2 würde nicht. Wenn es für Sie wichtig ist, REST-konform zu sein. Sie würden versuchen, PUT-Anrufe idempotent zu halten, da dies Sie vor vielen Kopfschmerzen bewahrt. Gleiches gilt für LÖSCHEN. Für befehlsähnliche Operationen würde ich dem Json- oder XML-RPC ehrlich gesagt eine Chance geben. Beide Strategien (REST und RPC) können in derselben Web-API zusammenleben. Es vermittelt mit dem Prinzip von CQRS (Command Query Responsibility Segregation :-)
Laiv

Antworten:

19

Sie können Ihre Drittanbieter Verkäufe an Ihr Produkt senden lassen. Zum Beispiel:

POST /product/{id}/sale { "Quantity": 3 }

Ich stimme sowohl Ihrem als auch dem Ihres Kollegen zu. Dies ist Geschäftslogik und sollte nicht dem Client der API überlassen werden, aber Sie sollten auch vermeiden, "Funktionen" als Endpunkte zu haben.

Manchmal ist es so einfach, solche Probleme zu lösen, wie es anders zu nennen, zugegebenermaßen nicht immer.

Robert Bräutigam
quelle
2
Diese. Plus: Anscheinend benötigt jeder Verkauf auch ein Objekt in der Datenbank. Jeder Verkauf als separates Objekt in db ermöglicht die Rückverfolgbarkeit. Denken Sie, wenn etwas schief gelaufen ist und die endgültige Lagermenge falsch ist und Werte festgelegt werden müssen. Wenn Sie nur eine Spalte mit dem Endwert haben, können Sie nicht viel tun. Hoffentlich gibt es nützliche Protokolle im System, um herauszufinden, was schief gelaufen ist. Wenn Sie Verkaufsobjekte mit Zeitstempeln, Benutzernamen und möglicherweise IP-Adresse angehängt haben, können Sie bestimmte Datensätze löschen, um Daten zu reparieren und zu verfolgen, von welchem ​​Benutzer / Standort sie stammen.
Ski
Vielen Dank für die Beiträge. Verkauf / Bestellung ist eine Ressource eines anderen Teams. Es liegt nicht in meiner Verantwortung, diese zu speichern oder zu verarbeiten. Wissen Sie, ist das Erstellen eines /saleEndpunkts noch gültig?
Sefa Ümit Oray
@ SefaÜmitOray: Die Endpunkte /saleund /product/{id}/salesind völlig unabhängig und die Tatsache, dass sie ähnliche Namen haben, bedeutet in keiner Weise, dass sie auf dieselbe Ressource verweisen.
Bart van Ingen Schenau
@BartvanIngenSchenau Was ich meine ist, saleist nicht in meiner Domain und es ist nicht Teil von product. Ist es immer noch sinnvoll zu erstellen, /product/{id}/salewährend es keine tatsächliche Ressource darstellt?
Sefa Ümit Oray
5
@ SefaÜmitOray Es ist völlig gültig, wenn es etwas Bedeutendes in Ihrem Kontext darstellt. Es muss nicht dasselbe bedeuten wie in anderen Kontexten, und es muss auch nichts sein, das direkt in der Datenbank gespeichert wird. Domain! = Datenbanktabellen, Ressource! = Datenbanktabellen.
Robert Bräutigam
3

Es gibt keinen Grund, warum Sie das auch nicht können. oder beides.

In einem Point-of-Sale-Kontext ist die Verfolgung einzelner Transaktionen sehr sinnvoll. Dort macht Roberts Lösung sehr viel Sinn.

In einem Lager- / Lagerkontext verfolgen Sie Transaktionen nicht unbedingt, sondern "Inventar". einen Endpunkt haben, über den der Kunde seine Lagerbestände melden kann

Ich habe 10 Einheiten Ich habe 7 Einheiten Ich habe 3 Einheiten Ich habe 20 Einheiten

macht sehr viel Sinn.

Die Lagerbestände ändern sich aus anderen Gründen als "Verkäufe". nur etwas zu beachten.

Theoretisch sollte der Lagerbestand aus den Änderungen berechenbar sein; In einigen Bereichen ist dies jedoch genau die Annahme, die Sie überprüfen möchten . Sie möchten in der Lage sein, den Lagerbestand auf zwei verschiedene Arten zu berechnen und auf Unstimmigkeiten zu prüfen (auch als "Schrumpfung" bezeichnet).

Daher denke ich nicht, dass die Semantik eindeutig ist, basierend auf dem von Ihnen angegebenen Kontext.

Wie für den HTTP-Teil; PUT [target-uri]Semantisch sinnvoll, wenn Sie eine Darstellung eines Dokuments durch eine andere ersetzen. Es ist ein UPSERT- der zweite PUT zu einer Ressource fordert zum Überschreiben der vorhandenen Darstellung auf.

PUT /sales { Quantity = 5 }
PUT /sales { Quantity = 2 }
PUT /sales { Quantity = 3 }

sagt, dass die Menge der verkauften Einheiten 3nicht ist 10.

PUT /sales/1 { Quantity = 5 }
PUT /sales/2 { Quantity = 2 }
PUT /sales/3 { Quantity = 3 }

So 10sieht es aus

PUT /sales { Quantity : [5] }
PUT /sales { Quantity : [5,2] }
PUT /sales { Quantity : [5,2,3] }

Das ist eine andere Art der Rechtschreibung 10.

POST /sales { Quantity = 5 }
POST /sales { Quantity = 2 }
POST /sales { Quantity = 3 }

Dies ist auch für HTTP akzeptabel. In einem unzuverlässigen Netzwerk ist dies jedoch keine gute Wahl, da Nachrichten manchmal doppelt vorhanden sind.

POST /sales { Quantity = 5 }
POST /sales { Quantity = 2 }
POST /sales { Quantity = 3 }
POST /sales { Quantity = 3 }

Ist das 13? oder 10?

PUT /sales/1 { Quantity = 5 }
PUT /sales/2 { Quantity = 2 }
PUT /sales/3 { Quantity = 3 }
PUT /sales/3 { Quantity = 3 }

Das ist eindeutig 10

PUT /sales { Quantity : [5,2,3] }
PUT /sales { Quantity : [5,2,3] }

Das ist eindeutig 10

PUT /sales/1 { Quantity = 5 }
PUT /sales/2 { Quantity = 2 }
PUT /sales/3 { Quantity = 3 }
PUT /sales/4 { Quantity = 3 }

Das ist eindeutig 13

PUT /sales { Quantity : [5,2,3] }
PUT /sales { Quantity : [5,2,3,3] }

Das ist eindeutig 13

POST /sales { TransactionId = 1 , Quantity = 5 }
POST /sales { TransactionId = 2 , Quantity = 2 }
POST /sales { TransactionId = 3 , Quantity = 3 }
POST /sales { TransactionId = 3 , Quantity = 3 }

10

POST /sales { TransactionId = 1 , Quantity = 5 }
POST /sales { TransactionId = 2 , Quantity = 2 }
POST /sales { TransactionId = 3 , Quantity = 3 }
POST /sales { TransactionId = 4 , Quantity = 3 }

13

(Um fair zu sein, HTTP unterstützt bedingte Anforderungen . Sie können einige der Metadaten aus Ihrem domänenspezifischen Protokoll in die domänenunabhängigen Header heben, um einige Unklarheiten zu beseitigen - wenn Sie den Client zum Mitspielen überreden können.)

Natürlich gibt es Kompromisse - HTML bietet keine native PUT-Unterstützung. Wenn Sie beabsichtigen, dass die Clients Ihrer API Browser sind, benötigen Sie entweder ein auf POST basierendes Protokoll oder Code-on-Demand-Erweiterungen, um die Formularübermittlung von einem POST in einen PUT zu konvertieren.

VoiceOfUnreason
quelle
1
Sie müssen die einzelnen Verkäufe nicht verfolgen , nur weil es einen Endpunkt dafür gibt. Dh es gibt keine Notwendigkeit , in der Lage sein , die Liste vorherigen Verkauf Anrufe, nur weil Sie es veröffentlichen können. Sie haben jedoch Recht, dass es andere Anwendungsfälle geben könnte (wir wissen es nicht), und Sie sollten idempotente Anrufe entweder mit bedingten Anrufen oder mit anderen Mitteln definieren.
Robert Bräutigam
2

Dies scheint ein wirklich schlechtes Design zu sein, egal wie Sie es schneiden. Ich würde niemals einem Dritten vertrauen, der mir mein aktuelles Inventar mitteilt, es sei denn, ich habe ihn mit der Verwaltung meines Lagers beauftragt.

Darüber hinaus ist der funktionsorientierte Ansatz überhaupt nicht REST-konform und führt bei Ihren Verbrauchern zu Bestürzung.

Schließlich kann ich mir kein Szenario vorstellen, in dem das einzige, was Sie an einem Verkauf interessiert, das resultierende Inventar ist, das Sie nach dessen Abschluss übrig haben.

Es ist viel besser, wenn der Dritte eine Verkaufs- oder Rechnungsressource an Sie sendet (mit Informationen wie Produkt, Menge, Datum, Versandart, Kundeninformationen usw.). Auf diese Weise können Sie tatsächlich analysieren und verfolgen, was Sie verkaufen, an wen, wann usw., damit Sie Ihr Unternehmen tatsächlich verwalten können.

Selbst wenn Ihr Dritter die vollständige Auftragserfüllung durchführt, möchten Sie den Umsatz nicht zuletzt für Zwecke der Buchhaltung und der Kundendemografie verfolgen.

Paul
quelle
1

PUT / api / Product / {Id} / --data {"StockQuantity": "{NewStockQuantity}"}

Diese Art von Design hat das Hauptproblem, dass Sie schmutzigen Lese- / Schreibvorgängen ausgesetzt sind, wenn Sie jemals mehr als einen Client-Thread für Ihre API ausführen möchten. Das heißt, zwischen dem Zeitpunkt, zu dem der Kunde die aktuelle Menge abruft und den neuen Wert berechnet, kann ein anderer Kunde denselben vorherigen Wert abrufen und eine andere Antwort berechnen. Die Menge, die Sie am Ende erhalten, ist diejenige, die zuletzt aktualisiert wurde, aber keine ist korrekt. Angenommen, Ihre aktuelle Menge ist 10. Kunde A möchte 5 Artikel verkaufen und zieht die aktuelle Menge. Gleichzeitig möchte Kunde B 6 Artikel verkaufen und zieht die aktuelle Menge. Beide sehen 10 Artikel auf Lager. A berechnet 5 verbleibende Elemente. B.berechnet 4 verbleibende. Beide aktualisieren. Sie zeigen jetzt 4 oder 5 verbleibende Elemente an, je nachdem, wer zuletzt aktualisiert wurde. Sie haben jedoch tatsächlich mehr Artikel verkauft, als Sie tatsächlich haben. Schlimmer ist, dass es keinen einfachen Weg gibt, durchzugehen und zu sehen, was schief gelaufen ist. Alles, was Sie haben, sind zwei falsche PUTsin Ihren Protokollen zu betrachten.

In jedem realen Aufzeichnungssystem ist es nicht ausreichend, nur eine aktuelle Summe zu haben. Überlegen Sie, ob Sie in ein Geschäft gehen und eine Reihe von Artikeln kaufen. Sie fordern eine Quittung an und die Kassiererin gibt Ihnen nur einen Zettel mit einem einzigen Gesamtbetrag. Wie würden Sie zeigen, dass die Gesamtsumme aus dieser Quittung korrekt ist? Wie würden Sie zeigen, dass Sie einen Artikel gekauft haben, wenn Sie etwas zurückgeben möchten?

Der Ansatz Ihres Freundes ist besser, aber ich würde vorschlagen, dem Mix eine Transaktions-ID hinzuzufügen. Dies behebt die wirklichen Bedenken, die VoiceOfUnreason in Bezug auf doppelte Transaktionen erwähnt. Eine Möglichkeit besteht darin, eine POSTOperation zum Erstellen einer neuen Transaktion bereitzustellen und diese dann PUTzu bestätigen. Zum Zeitpunkt der Bestätigung reduzieren Sie den Gesamtbestand oder lehnen die Anfrage ab, da nicht genügend verfügbar ist.

JimmyJames
quelle
1

Da Verkäufe von Dritten abgewickelt werden, müssen Sie die Kontrolle über Ihren Produktbestand haben, indem Sie nicht zulassen, dass diese die Lageranzahl aktualisieren.

Für den internen Gebrauch, z. B. zur Bestandszählung, können Sie Ihren Ansatz festlegen, d PUT /api/Product/{Id}/ --data { "StockQuantity": "{NewStockQuantity}" }. H.

Für den externen Gebrauch müssen Sie eine separate Schnittstelle erstellen, z. B. /api/SalesOrder/eine Liste von Produkten und Mengen, wie z.

POST /api/SalesOrder/ --data { [{"Id": 1, "Qty": 1}, {"Id": 2, "Qty": 3}] }

Basierend auf SalesOrderden von Dritten gesendeten Daten kann die Menge jedes Produkts aktualisiert und der Bestellung zugewiesen werden, oder Sie können die Bestellung ablehnen, wenn nicht genügend Produkte verfügbar sind.

Die Verarbeitung und Bestandszählung erfolgt intern. Dritte benötigen lediglich eine Schnittstelle, damit sie ihre Bestellungen an das Inventar weiterleiten können. Grundsätzlich SalesOrderkommunizieren Vertrieb, Finanzen und Lager auf diese Weise, um einen Verkauf abzuschließen.

imel96
quelle