REST API - wiele endpointów zmiany statusu z różnymi parametrami

1

Hej wszystkim,
Probuje znalezc najlepsze rozwiazanie dla zadesignowania REST API, ale niestety wiekszosc artykulow opisuje proste interfejsy. W moim przypadku mam obecnie endpoint do zmiany statusu zamowienia

PUT <url>/orders/{orderID}

zamowienie moze miec rozne statusy (zamkniete, oczekujace, zablokowane, wyslane do weryfikacji). Problem w tym ze dla kazdego z tych statusow roznia sie parametry ktore API przyjmuje (np zmiana statusu na oczekujace wymaga przekazania localdatetime z czasem oczekiwania. Natomiast zmiana status na zamkniete nie wymaga podania zadnego czasu). W wyniku tego powstala wielka metoda w kontrolerze ktora przyjmuje wszystkie mozliwe parametry dla wszystkich statusow:

                       @RequestParam Status status,
                       @RequestParam String blockerReason,
                       @RequestParam Approver approverPerson,
                       @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endTime)

Moze wyglada to spoko z poziomu API (jeden endpoint), ale bardzo mi sie nie podoba w kodzie. Metoda przyjmuje wiele parametrow z ktorych wiekszosc jest nie wykorzystana. Na poczatku trzeba zrobic ifa sprawdzajac status i potem uzyc parametry potrzebne dla tego procesu.

Chcialbym to rozbic na mniejsze metody z mniejsza iloscia parametrow z ktorych wszystkie sa potrzebne. Przykladowo, metoda zmieniajaca status na oczekujace przyjmowalaby jedynie dwa potrzebne parametry:

     @PutMapping(value = "/{...}", produces = {"application/json"})
    public void changeToWating(@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endTime) {

Nie byloby tez koniecznosci sprawdzania jaki jest status tak jak aktualnie, bo byloby to API do zmieniania statusu na oczekujace, wiec wiemy ze status powinien byc ustawiony na oczekujace.
Problem jest taki ze nie wiem jak zaprojektowac API pod to. Wiekszosc dobrych praktych mowi ze nie powinno uzywac sie czasownikow, wiec API w stylu /orders/{orderId}/changestatus/waiting raczej nie wyglada dobrze.

Mieliscie moze podobne przypadki? Jak je rozwiazaliscie?

1
platinium napisał(a):

. Wiekszosc dobrych praktych mowi ze nie powinno uzywac sie czasownikow, wiec API w stylu /orders/{orderId}/changestatus/waiting raczej nie wyglada dobrze.

w REST API zwany też niesłusznie Web API
Ale to tylko jeden z rodzajów. Niestety w głowach panuje monokultura, i węższą koncepcję się utożsamia z szerszą (ogólną)

W API jak najbardziej mogą być czasowniki, a nawet powinny.

Problem jest taki, że w REST literka S oznacza State. Los może przynieść dwa rozwiązania:
a) tak przemyśleć State, być może w innej klasie da się o tym mówić jako o rzeczowniku. To czasem będzie mówienie jak Kali, ale sie da bez czasowników
b) zgwałcony REST, nie twój pierwszy, nie ostatni

W mojej opinii wolne od gwałcenia jest modelowanie w jakiejś formie RPC (bo czasownik jest tam jak najbardziej OK). O historycznych i nowoczesnych RPC można długo gadać.

9

Widzisz jedynie proste tutoriale, bo REST jest do rozwiązywania prostych problemów. Z natury sam w sobie dotyczy jedynie prostych (CRUD) operacji, na hierarchicznie zorganizowanych zbiorach danych i praktycznie każde wyjście poza te ramy powoduje, że projektant czuje się, jak gdyby próbował naprawiać zegarek młotkiem, albo wbijać gwoździe zegarkiem go:

  • W rzeczywistych zastosowaniach mamy jakiś graf relacji, a ujęcie hierarchiczne jest tylko jedną z wielu perspektyw dla każdego grafu.
  • POST, GET, PUT, DELETE mają się mapować 1:1 na CRUD, a jak przychodzi do "zwiększ o 10% cenę wszystkich niebieskich spodni", to okazuje się, że "czegoś zabrakło".

W wyniku tego, REST jest takim schematem o którym wszyscy mówią, a właściwie nikt go nie widział w czystej formie.

W kwestii praktycznej, jeżeli stajesz przed wyborem "czytelne API", a "czytelny kod", to zawsze trzeba wybrać bramkę #1. To trochę taki problem jak z UI i zorganizujmy je inaczej, bo tak nam bardziej pasuje do "mój kod taki piękny".

1

@piotrpo:

Pięknie napsiane, będę powtarzał.

Jakoś tak jest, że historii polityczno-gospodarczej przychodzi kiedyś czas, ze następuje opisanie DALCZEGO kraj X kretyńsko wyciął swoją bioróżnorodność, i pokrył 90% ziemi uprawnej kukurydzą / bananem / soją. Wiemy jakie ambicje polityków za tym stały, jakie problemy państw NIBY to miało uzdrowić, jaki szarlatan pseudonaukowiec zdobył zbyt mocną pozycję (np Miczurin), jak "przekonywano" niepokornych itd

Ale my chyba nie doczekamy się takiego opracowania, dlaczego REST zdominował i zabił inne formaty integracji.

Ludzie uniwersytetów zawsze będą proponować fajne na tablicy uproszczone modele (w sumie każdy model we wszechświecie jest uproszczeniem życia), do Roya Fieldinga nic nie mam ... nawiasem mówiąc nie spotkałem się z jakąkolwiek jego publiczną wypowiedzią, jak Ojciec ocenia to co naprawdę się z jego dzieckiem REST-em robi w realnej praktyce korporacyjnej.
Jak ktoś z was trafił na takową, pls podać linkę

5

zamowienie moze miec rozne statusy (zamkniete, oczekujace, zablokowane, wyslane do weryfikacji). Problem w tym ze dla kazdego z tych statusow roznia sie parametry ktore API przyjmuje (np zmiana statusu na oczekujace wymaga przekazania localdatetime z czasem oczekiwania. Natomiast zmiana status na zamkniete nie wymaga podania zadnego czasu). W wyniku tego powstala wielka metoda w kontrolerze ktora przyjmuje wszystkie mozliwe parametry dla wszystkich statusow:

Właśnie dla tego coraz częściej powtarzam że REST wcale nie powinno być domyślnym wyborem, a ludzie błędnie używają REST jako synonim dobrze napisanego API. Wszystko ma swoje zastosowania, oraz takie scenariusze gdzie się coś nie sprawdza.

Moim zdaniem to czego potrzebujesz to task-based API. W przypadku który opisujesz będziesz miał nowy endpoint dla odpowiedniej operacji (np. zamknięcie zamówienia), z DTO stworzonym pod ten konkretny endpoint.

0
Aventus napisał(a):

Właśnie dla tego coraz częściej powtarzam że REST wcale nie powinno być domyślnym wyborem, a ludzie błędnie używają REST jako synonim dobrze napisanego API. Wszystko ma swoje zastosowania, oraz takie scenariusze gdzie się coś nie sprawdza.

Moim zdaniem to czego potrzebujesz to task-based API. W przypadku który opisujesz będziesz miał nowy endpoint dla odpowiedniej operacji (np. zamknięcie zamówienia), z DTO stworzonym pod ten konkretny endpoint.

Nie żebym chciał być adwokatem diabła, ale przypominają mi sie trzy literki RPC (niekoniecznie implementacje, niektóre są bardzo przeżyte, ale ta grupa sobie dzielnie choć niszowo żyje)
Taki jest Apache Thrift (wręcz postulowane DTO jest klasą/strukturą) czy gRPC
Współczesne RPC całkiem dobrze się bronią przed zmiennością / rozwojem protokołu, co apostołowie REST/JSON próbują rozgrywać jako zaletę specyficzną jedynie dla tego środowska.

1

@ZrobieDobrze: Żeby nie było, ja uważam REST za fajną sprawę, jedynie nie uważam, że jego jak to wyżej określiłeś "gwałcenie" jest czymś, czego należy unikać za wszelką cenę. Oczywiście CRUD nie pasuje 1:1 do większości aplikacji, ale z drugiej strony, jest w stanie pokryć zdecydowaną większość operacji i zostaje parę, gdzie trzeba trochę naciągnąć kołderkę. Przykładem są te wyżej wymienione "czasowniki".
Ja nie mam nic przeciwko POST /goods/pants/command?color=blue, ani POST /goods/pants/multiplyPrice/1.1?color=blue jak ktoś sobie przyjmie taką spójną konwencję w swoim projekcie.

Ogromną zaletą REST, jest to, że jest to prosta, popularna konwencja. To upraszcza podłączenie się do takiego API. Gorzej, jak ktoś zaczyna marudzić, że nie możemy np. zaimplementować wyszukiwania po obrazku, bo wyszukiwanie musi być parametrem query, a zakodowana bitmapa wyskoczy poza limit.

0

to co piszę @piotrpo

POST <url>/orders/{orderID}/change_status/1
POST <url>/orders/{orderID}/change_status/2
POST <url>/orders/{orderID}/change_status/3

wady:

hardcoded url, gdy nastąpią zmiany w statusach to trzeba będzie pamiętać o zmianach

traci się wygodę z perspektywy wołającego API

zalety:

nie wymaga dużego przemeblowania w appce,

przejrzyste API i jego input w np. swaggerze,

wysyłane są tylko potrzebne rzeczy

edit.

A gdybyś nadal miał 1 endpoint który zbiera wszystko, a w jego implementacji sobie to rozbił?

switch (nextStatus)
{
	case 1: return handleNew(id, param1);
	case 2: return handleAccepted(id, param2, param3);
	case 3: return handleModified(id, param4);	
	case 4: return hadneClosed(id);
}
1

Cześć, jak masz taki case to ja bym użył polimorfizmu.
Tworzysz request Typu Order, i dla niego klasy pochodne ClosedOrder, PendingOrder itd.
Teraz używasz tej bibliotaki: https://github.com/infobip/infobip-jackson-extension

I masz sytuacje, gdzie do kontrolera przychodzi Order i w zależności od tego, jakiego jest typu tak go obsługujesz.
Możesz użyć stwich case, ale ja bym użył wzorca strategii.

1 użytkowników online, w tym zalogowanych: 0, gości: 1