Implementowanie wzorca poleceń w interfejsie API RESTful

12

Jestem w trakcie projektowania interfejsu API HTTP, mam nadzieję, że uczynię go możliwie jak najbardziej REST.

Istnieje kilka działań, których funkcjonalność rozciąga się na kilka zasobów i czasem trzeba je cofnąć.

Pomyślałem sobie, że to brzmi jak wzorzec poleceń, ale jak mogę zamodelować go w zasób?

Przedstawię nowy zasób o nazwie XXAction, taki jak DepositAction, który zostanie utworzony poprzez coś takiego

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

spowoduje to utworzenie nowej operacji DepositAction i aktywację jej metody Do / Execute. W takim przypadku zwrócenie statusu 201 Utworzono HTTP oznacza, że ​​akcja została wykonana pomyślnie.

Później, jeśli klient chce przyjrzeć się szczegółom akcji, może

GET /action/{action-id}

Wydaje mi się, że aktualizacja / PUT powinna zostać zablokowana, ponieważ tutaj nie ma to znaczenia.

Aby cofnąć akcję, pomyślałem o użyciu

DELETE /action/{action-id}

który faktycznie wywoła metodę Cofnij odpowiedniego obiektu i zmieni jego status.

Powiedzmy, że jestem zadowolony z tylko jednego cofania, nie muszę tego robić ponownie.

Czy to podejście jest w porządku?

Czy są jakieś pułapki, powody, aby z niego nie korzystać?

Czy to rozumie się z POV klientów?

Mithir
źródło
Krótka odpowiedź, to nie jest REST.
Evan Plaice
3
@EvanPlaice chcesz to rozwinąć? to jest dokładnie pytanie.
Mithir
1
Opracowałbym odpowiedź, ale odpowiedź Gary'ego obejmuje już większość / całość tego, co dodam. Mówię, że to nie jest odpoczynek, ponieważ identyfikatory URI mają jedynie reprezentować zasoby (tj. Nie akcje). Akcje są obsługiwane przez GET / POST / PUT / DELETE / HEAD. Pomyśl o REST jako o interfejsie OOP. Celem jest dopasowanie interfejsu API do ogólnego wzorca i oddzielenie go od szczegółów związanych z implementacją, jak to możliwe.
Evan Plaice,
1
@EvanPlaice Ok, rozumiem, dziękuję.
Wydaje
W takim przypadku identyfikator URI powinien reprezentować transakcję, w której obciążanie (pobieranie pieniędzy) i uznawanie (przekazywanie pieniędzy) są czynnościami wykonywanymi za pośrednictwem żądań POST. POST jest używany w obu przypadkach, ponieważ za każdym razem, gdy pieniądze są przenoszone w dowolnym kierunku, oznacza to powstanie nowej transakcji. W konkretnym przypadku transakcje odbywają się na koncie posiadacza karty, więc numer konta karty jest identyfikatorem URI zasobu.
Evan Plaice,

Odpowiedzi:

13

Dodajesz warstwę abstrakcji, która jest myląca

Twój interfejs API zaczyna się bardzo czysty i prosty. HTTP POST tworzy nowy zasób depozytowy z podanymi parametrami. Następnie zejdziesz z torów, wprowadzając ideę „działań”, które są szczegółami implementacyjnymi, a nie podstawową częścią interfejsu API.

Alternatywnie rozważ tę rozmowę HTTP ...

POST / card / {card-id} / account / {account-id} / Depozyt

AmountToDeposit = 100, różne parametry ...

201 STWORZONY

Lokalizacja = / karta / 123 / konto / 456 / Depozyt / 789

Teraz chcesz cofnąć tę operację (technicznie nie powinno to być dozwolone w zrównoważonym systemie księgowym, ale hej):

USUŃ / karta / 123 / konto / 456 / Depozyt / 789

204 BRAK TREŚCI

Klient interfejsu API wie, że ma do czynienia z zasobem depozytowym i jest w stanie określić, jakie operacje są na nim dozwolone (zwykle poprzez OPCJE w HTTP).

Chociaż wdrożenie operacji usuwania odbywa się dzisiaj za pomocą „akcji”, nie ma gwarancji, że migracja tego systemu z, powiedzmy, C # do Haskell i utrzymanie interfejsu użytkownika, że ​​wtórna koncepcja „akcji” nadal będzie stanowić wartość dodaną , podczas gdy podstawowa koncepcja Depozytu na pewno działa.

Edytuj, aby objąć alternatywę dla USUŃ i Depozyt

Aby uniknąć operacji usuwania, ale nadal skutecznie usuwać Depozyt, wykonaj następujące czynności (używając ogólnej Transakcji, aby umożliwić Depozyt i Wypłatę):

POST / card / {card-id} / account / {account-id} / Transaction

Kwota = -100 , różne parametry ...

201 STWORZONY

Lokalizacja = / karta / 123 / konto / 456 / Transacja / 790

Tworzony jest nowy zasób transakcji, który ma dokładnie przeciwną kwotę (-100). Powoduje to równoważenie konta z powrotem do 0, co neguje pierwotną transakcję.

Możesz rozważyć utworzenie punktu końcowego typu „narzędzie”

POST / card / {card-id} / account / {account-id} / Transaction / 789 / Undo <- BAD!

aby uzyskać ten sam efekt. Jednak łamie to semantykę identyfikatora URI jako identyfikatora poprzez wprowadzenie czasownika. Lepiej trzymaj się rzeczowników w identyfikatorach i ograniczaj operacje do czasowników HTTP. W ten sposób możesz łatwo utworzyć permalink z identyfikatora i używać go do GET i tak dalej.

Gary Rowe
źródło
3
+1 „technicznie nie powinno to być dozwolone w zrównoważonym systemie księgowym”. Ktoś wie, jak liczyć fasolę. To oświadczenie jest absolutnie poprawne, sposobem na odwrócenie byłoby utworzenie kolejnej transakcji zasilającej zwrot środków. Wpisy do księgi głównej powinny być zawsze uważane za niezmienne i trwałe po zakończeniu transakcji.
Evan Plaice
Więc jeśli zmienię, w moich pytaniach zamiast Usuń / akcja / ... na Usuń / wpłać / ... czy jest w porządku?
Mithir
2
@Mithir opisywałem zasadę rachunkowości. W standardowym systemie księgowym z podwójnym wpisem nigdy nie usuwasz transakcji. Raz popełniona historia jest uważana za niezmienną, aby ludzie byli uczciwi. W twoim przypadku nadal możesz użyć akcji DELETE, ale na zapleczu (np. Tabela bazy danych księgi głównej) dodajesz kolejną transakcję reprezentującą kredytowanie (tj. Oddawanie) pieniędzy użytkownikowi. Nie jestem licznikiem fasoli (tj. Księgowym), ale jest to jedna ze standardowych praktyk nauczanych na kursie „Zasady rachunkowości I”.
Evan Plaice,
2
(ciąg dalszy) Dzienniki bazy danych wykorzystują transakcje w podobny sposób. Dlatego możliwe jest zreplikowanie i / lub przebudowanie zestawu danych przy użyciu tylko dzienników. Tak długo, jak transakcje są odtwarzane w sposób chronologiczny, powinna istnieć możliwość odbudowania zestawu danych z dowolnego punktu w jego historii. Usunięcie zmienności z równania zapewnia spójność.
Evan Plaice,
1
Wystarczy, że zmienisz nazwę na Transaction.
Gary Rowe,
1

Głównym powodem istnienia REST jest odporność na błędy sieciowe. W tym celu wszystkie operacje powinny być idempotentne .

Podstawowe podejście wydaje się rozsądne, ale sposób, w jaki opisujesz DepositActionstworzenie, nie wydaje się idempotentny, co powinno zostać naprawione. Klient musi podać unikalny identyfikator, który będzie używany do wykrywania duplikatów żądań. Tak więc stworzenie zmieni się na

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

Jeśli zostanie wykonane inne PUT dla tego samego adresu URL z taką samą treścią jak poprzednio, odpowiedź powinna nadal brzmieć, 201 createdjeśli treść jest taka sama, i błąd, jeśli treść jest inna. Dzięki temu klient może po prostu retransmitować żądanie, gdy się nie powiedzie, ponieważ klient nie może stwierdzić, czy żądanie lub odpowiedź zaginęły.

Bardziej sensowne jest użycie PUT, ponieważ po prostu zapisuje zasób i jest idempotentny, ale użycie POST tak naprawdę nie spowoduje żadnego problemu.

Aby zobaczyć szczegóły transakcji, klient będzie miał GETten sam adres URL, tj

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

i aby go cofnąć, można go usunąć. Ale jeśli faktycznie ma to coś wspólnego z pieniędzmi, jak sugeruje próbka, sugerowałbym PUT dodawanie flag „anulowanych” zamiast tego ze względu na odpowiedzialność (że pozostaje ślad utworzonej i anulowanej transakcji).

Teraz musisz wybrać metodę tworzenia unikalnego identyfikatora. Masz kilka opcji:

  1. Wydawaj wcześniej w wymianie specyficzny dla klienta prefiks, który należy uwzględnić.
  2. Dodaj specjalne żądanie POST, aby uzyskać pusty unikalny identyfikator z serwera. To żądanie nie musi być idempotentne (i nie może tak naprawdę), ponieważ nieużywane identyfikatory nie powodują żadnych problemów.
  3. Po prostu użyj UUID. Wszyscy ich używają i wydaje się, że nikt nie ma problemu z tymi opartymi na MACie ani z przypadkowymi.
Jan Hudec
źródło
2
Z tego co wiem, POST nie jest idempotentny. en.wikipedia.org/wiki/POST_(HTTP)#Affecting_server_state
Mithir
@Mithir: POST nie jest idempotentny; wciąż może być. Ale prawdą jest, że ponieważ wszystkie operacje REST mają być idempotentne, POST zasadniczo nie ma miejsca w REST.
Jan Hudec
1
Jestem zdezorientowany ... treść, którą przeczytałem i istniejąca implementacja, którą znam (ServiceStack, ASP.NET Web API), wszystko sugeruje, że POST ma miejsce w REST.
Mithir
3
W REST idempotencja jest przypisywana do zasobu, a nie do protokołu lub jego kodów odpowiedzi. Zatem w REST przez HTTP metody GET, PUT, DELETE, PATCH i tak dalej są uważane za idempotentne, chociaż ich kody odpowiedzi mogą się różnić dla kolejnych wywołań. POST jest idempotentny w tym sensie, że każde połączenie tworzy nowy zasób. Zobacz Fielding. Używanie POST jest OK .
Gary Rowe,
1
Operacje, które nie są idempotentne, są dozwolone w spoczynku. To twierdzenie jest całkowicie błędne.
Andy,