Wywołaj metodę po stronie serwera dla zasobu w sposób RESTful


142

Pamiętaj, że mam podstawowe rozumienie REST. Powiedzmy, że mam ten adres URL:

http://api.animals.com/v1/dogs/1/

A teraz chcę, aby kelner powodował szczekanie psa. Tylko serwer wie, jak to zrobić. Powiedzmy, że chcę, aby działał w trybie CRON, który powoduje, że pies szczeka co 10 minut przez resztę wieczności. Jak wygląda to wezwanie? W pewnym sensie chcę to zrobić:

Żądanie adresu URL:

ACTION http://api.animals.com/v1/dogs/1/

W treści żądania:

{"action":"bark"}

Zanim zdenerwujesz się na mnie, że stworzyłem własną metodę HTTP, pomóż mi i daj mi lepszy pomysł, jak powinienem wywołać metodę po stronie serwera w RESTful sposób. :)

EDYTUJ W CELU WYJAŚNIENIA

Więcej wyjaśnień na temat tego, co robi metoda „kory”. Oto kilka opcji, które mogą skutkować różną strukturą wywołań interfejsu API:

  1. szczeka po prostu wysyła e-mail na adres dog.email i nic nie rejestruje.
  2. bark wysyła e-mail na adres dog.email i zwiększa liczbę dog.barkCount o 1.
  3. bark tworzy nowy rekord „bark” z zapisem bark.timestamp, kiedy nastąpiła kora. Zwiększa również liczbę dog.barkCount o 1.
  4. bark uruchamia polecenie systemowe, aby pobrać najnowszą wersję kodu psa z Githuba. Następnie wysyła wiadomość tekstową do właściciela psa, informując go, że nowy kod psa jest w produkcji.

14
Co ciekawe, dodanie nagrody wydaje się przyciągać gorsze odpowiedzi niż pierwotnie ;-) Podczas oceny odpowiedzi pamiętaj, że: 1) Specyfikacje czasowników HTTP wykluczają wybór inny niż POST. 2) REST nie ma nic wspólnego ze strukturą adresu URL - jest to ogólna lista ograniczeń (bezstanowych, buforowalnych, warstwowych, jednolitych interfejsów itp.), Niż zapewnia korzyści (skalowalność, niezawodność, widoczność itp.). 3) Obecna praktyka (np. Używanie POST w specyfikacjach RPC) jest ważniejsza od definicyjnych, którzy tworzą własne reguły API. 4) REST wymaga jednolitego interfejsu (zgodnie ze specyfikacją HTTP).
Raymond Hettinger

@Kirk, co myślisz o nowych odpowiedziach? Czy jest coś, co nadal chcesz wiedzieć, ale nie zostało poruszone w żadnym z nich? Byłbym bardziej niż szczęśliwy, mogąc ponownie edytować moją odpowiedź, gdyby byłaby bardziej pomocna.
Jordan

@RaymondHettinger PATCHmoże być odpowiedni. Pod koniec mojej odpowiedzi wyjaśniam dlaczego .
Jordania

PATCH byłby odpowiedni tylko do zwiększenia liczby dog.barkCount o jeden. POST to metoda wysyłania wiadomości e-mail, tworzenia nowego rekordu kory, uruchamiania poleceń do pobrania z Github lub wyzwalania wiadomości tekstowej. @Jordan, twoje odczytanie PATCH RFC jest pomysłowe, ale nieco sprzeczne z jego intencją jako wariant PUT do częściowej modyfikacji zasobów. Myślę, że nie pomagasz OP, wymyślając niekonwencjonalne odczyty specyfikacji HTTP, zamiast potwierdzać standardową praktykę używania POST do zdalnego wywoływania procedur.
Raymond Hettinger

@RaymondHettinger, którego praktyka de facto standaryzuje POST? Wszystkie standardowe interfejsy RPC, które widziałem, identyfikują zasób według jednostki (nie RESTful), a nie URI, więc prawidłowa odpowiedź określająca priorytety RPC i tak musiałaby być niekonwencjonalna, co moim zdaniem obala wartość konwencjonalnego RPC: jeden jest pomysłowy lub niespójny . Nigdy nie możesz się pomylić z POST, ponieważ jest to catch-all do przetwarzania danych, ale istnieją bardziej szczegółowe metody. REST oznacza nazywanie zasobów i opisywanie zmian ich stanu, a nie nazywanie procedur zmieniających stan. PATCH i POST opisują zmiany stanu.
Jordan,

Odpowiedzi:


280

Dlaczego warto dążyć do projektu RESTful?

Zasady RESTful wprowadzają funkcje, które sprawiają, że strony internetowe są łatwe (dla losowego użytkownika do „surfowania” po nich) do projektu API usług sieciowych , dzięki czemu są one łatwe w użyciu dla programisty. REST nie jest dobry, ponieważ jest REST, jest dobry, ponieważ jest dobry. I to jest dobre głównie dlatego, że jest proste .

Prostota zwykłego protokołu HTTP (bez kopert SOAP i POSTusług przeciążonych jednym identyfikatorem URI ), co niektórzy mogą nazwać „brakiem funkcji” , jest w rzeczywistości jego największą zaletą . Od samego początku HTTP prosi o adresowalność i bezpaństwowość : dwie podstawowe decyzje projektowe, które zapewniają skalowalność HTTP do dzisiejszych mega-witryn (i mega-usług).

Ale REST nie jest najlepszym rozwiązaniem: czasami może być odpowiedni styl RPC („zdalne wywołanie procedury” - na przykład SOAP) , a czasami inne potrzeby mają pierwszeństwo przed zaletami sieci Web. Jest okej. To, czego tak naprawdę nie lubimy, to niepotrzebna złożoność . Zbyt często programista lub firma sprowadza usługi w stylu RPC do zadań, z którymi zwykły stary HTTP mógłby sobie poradzić. Efekt jest taki, że HTTP jest zredukowane do protokołu transportowego dla olbrzymiego ładunku XML, który wyjaśnia, co się „naprawdę” dzieje (nie URI ani metoda HTTP dają o tym wskazówkę). Wynikowa usługa jest zbyt złożona, niemożliwa do debugowania i nie będzie działać, jeśli Twoi klienci nie będą mieli dokładnej konfiguracji zgodnie z zamierzeniami programisty.

W ten sam sposób kod Java / C # nie może być zorientowany obiektowo, samo użycie protokołu HTTP nie czyni projektu zgodnym z REST. Można złapać się w pośpiechu myślenia o swoich usługach w kategoriach działań i metod zdalnych, które należy nazwać. Nic dziwnego, że skończy się to głównie w usłudze w stylu RPC (lub w hybrydzie REST-RPC). Pierwszym krokiem jest myślenie inaczej. Projekt zgodny ze standardem REST można osiągnąć na wiele sposobów, jednym ze sposobów jest myślenie o aplikacji w kategoriach zasobów, a nie działań:

💡 Zamiast myśleć w kategoriach działań, które może wykonać („wyszukaj miejsca na mapie”) ...

... spróbuj myśleć kategoriami wyników tych działań („lista miejsc na mapie spełniających kryteria wyszukiwania”).

Poniżej przedstawię przykłady. (Innym kluczowym aspektem REST jest użycie HATEOAS - nie szczotkuję go tutaj, ale mówię o tym szybko w innym poście .)


Zagadnienia pierwszego projektu

Rzućmy okiem na proponowany projekt:

ACTION http://api.animals.com/v1/dogs/1/

Po pierwsze, nie powinniśmy rozważać tworzenia nowego czasownika HTTP ( ACTION). Ogólnie rzecz biorąc, jest to niepożądane z kilku powodów:

  • (1) Biorąc pod uwagę tylko identyfikator URI usługi, w jaki sposób „przypadkowy” programista będzie wiedział, że ACTIONczasownik istnieje?
  • (2) jeśli programista wie, że istnieje, jak pozna jego semantykę? Co oznacza ten czasownik?
  • (3) jakie właściwości (bezpieczeństwo, idempotencja) powinien mieć ten czasownik?
  • (4) co zrobić, jeśli programista ma bardzo prostego klienta, który obsługuje tylko standardowe czasowniki HTTP?
  • (5) ...

Rozważmy teraz użyciePOST (poniżej omówię dlaczego, po prostu uwierz mi na słowo):

POST /v1/dogs/1/ HTTP/1.1
Host: api.animals.com

{"action":"bark"}

To mogłoby być OK ... ale tylko wtedy, gdy :

  • {"action":"bark"}był dokumentem; i
  • /v1/dogs/1/był identyfikatorem URI „procesora dokumentów” (podobnym do fabrycznego). „Procesor dokumentów” to URI, do którego po prostu „wrzucasz” i „zapomnisz” o nich - procesor może przekierować cię do nowo utworzonego zasobu po „rzuceniu”. Np. URI do wysyłania wiadomości w usłudze brokera komunikatów, który po wysłaniu przekierowuje do URI, który pokazuje stan przetwarzania wiadomości.

Nie wiem zbyt wiele o twoim systemie, ale założę się, że oba nie są prawdziwe:

  • {"action":"bark"} nie jest dokumentem , w rzeczywistości jest to metoda, którą próbujesz wkraść się do serwisu; i
  • /v1/dogs/1/URI oznacza „pies” zasób (prawdopodobnie z psem id==1), a nie procesora dokumentów.

Więc wszystko, co teraz wiemy, to to, że powyższy projekt nie jest tak RESTful, ale co to dokładnie jest? Co w tym złego? Zasadniczo jest to złe, ponieważ jest to złożony identyfikator URI o złożonych znaczeniach. Nic z tego nie można wywnioskować. Skąd programista miałby wiedzieć, że pies ma barkakcję, którą można potajemnie wprowadzić POSTdo niego?


Projektowanie wywołań API pytania

Przejdźmy więc do sedna i spróbujmy zaprojektować te szczekanie RESTO, myśląc w kategoriach zasobów . Pozwólcie mi zacytować książkę Restful Web Services :

POSTWniosek jest próbą stworzenia nowego zasobu od istniejącej. Istniejący zasób może być rodzicem nowego w sensie struktury danych, tak jak korzeń drzewa jest rodzicem wszystkich jego węzłów liści. Lub istniejący zasób może być specjalnym zasobem „fabrycznym”, którego jedynym celem jest generowanie innych zasobów. Reprezentacja wysłana wraz z POSTżądaniem opisuje początkowy stan nowego zasobu. Podobnie jak w przypadku PUT, POSTwniosek nie musi w ogóle zawierać reprezentacji.

W następstwie powyższego opisu widać, że barkmożna modelować jako o subresource Urządzonydog (ponieważ barkzawiera się w psa, to jest kora jest „szczekaly” przez psa).

Z tego rozumowania już mamy:

  • Metoda jest taka POST
  • /barksZasobem jest podźródło psa:, /v1/dogs/1/barksreprezentujące bark„fabrykę”. Ten identyfikator URI jest unikalny dla każdego psa (ponieważ znajduje się poniżej /v1/dogs/{id}).

Teraz każdy przypadek na twojej liście ma określone zachowanie.

1. kora po prostu wysyła e-mail dog.emaili nic nie rejestruje.

Po pierwsze, czy szczekanie (wysyłanie wiadomości e-mail) jest zadaniem synchronicznym czy asynchronicznym? Po drugie, czy barkżądanie wymaga jakiegoś dokumentu (może e-mail), czy jest puste?


1.1 kora wysyła e-mail do dog.email i nic nie rejestruje (jako zadanie synchroniczne)

Ta sprawa jest prosta. Wezwanie do barkszasobów fabryki powoduje natychmiastowe wyświetlenie kory (wysłanie wiadomości e-mail), a odpowiedź (jeśli jest OK lub nie) jest natychmiast udzielana:

POST /v1/dogs/1/barks HTTP/1.1
Host: api.animals.com
Authorization: Basic mAUhhuE08u724bh249a2xaP=

(entity-body is empty - or, if you require a **document**, place it here)

200 OK

Ponieważ nic nie rejestruje (nie zmienia), 200 OKto wystarczy. Pokazuje, że wszystko poszło zgodnie z oczekiwaniami.


1.2 Bark wysyła e-mail dog.emaili nic nie rejestruje (jako zadanie asynchroniczne)

W takim przypadku klient musi mieć możliwość śledzenia barkzadania. barkNastępnie zadanie powinno być zasobem z własnym URI .:

POST /v1/dogs/1/barks HTTP/1.1
Host: api.animals.com
Authorization: Basic mAUhhuE08u724bh249a2xaP=

{document body, if needed;
NOTE: when possible, the response SHOULD contain a short hypertext note with a hyperlink
to the newly created resource (bark) URI, the same returned in the Location header
(also notice that, for the 202 status code, the Location header meaning is not
standardized, thus the importance of a hipertext/hyperlink response)}

202 Accepted
Location: http://api.animals.com/v1/dogs/1/barks/a65h44

W ten sposób każdy barkjest identyfikowalny. Klient może następnie wydać GETna barkURI, by poznać jego aktualny stan. Może nawet użyj, DELETEaby to anulować.


2. kora wysyła e-mail do, dog.emaila następnie zwiększadog.barkCount o 1

To może być trudniejsze, jeśli chcesz poinformować klienta, że dogzasób zostanie zmieniony:

POST /v1/dogs/1/barks HTTP/1.1
Host: api.animals.com
Authorization: Basic mAUhhuE08u724bh249a2xaP=

{document body, if needed; when possible, containing a hipertext/hyperlink with the address
in the Location header -- says the standard}

303 See Other
Location: http://api.animals.com/v1/dogs/1

W tym przypadku locationintencją nagłówka jest poinformowanie klienta, że ​​powinien się przyjrzeć dog. Z HTTP RFC o303 :

Ta metoda istnieje głównie po to, aby dane wyjściowe POSTaktywowanego skryptu przekierowywały agenta użytkownika do wybranego zasobu.

Jeśli zadanie jest asynchroniczne, barkzasób podrzędny jest potrzebny tak jak 1.2sytuacja i 303powinien zostać zwrócony GET .../barks/Ypo zakończeniu zadania.


3. Kora tworzy nowy " bark" rekord z bark.timestampnagraniem, kiedy nastąpiło szczekanie. Zwiększa się również dog.barkCounto 1.

POST /v1/dogs/1/barks HTTP/1.1
Host: api.animals.com
Authorization: Basic mAUhhuE08u724bh249a2xaP=

(document body, if needed)

201 Created
Location: http://api.animals.com/v1/dogs/1/barks/a65h44

Tutaj barkjest utworzony w wyniku żądania, więc status 201 Createdjest stosowany.

Jeśli tworzenie jest asynchroniczne, zamiast tego 202 Acceptedwymagana jest litera ( zgodnie z dokumentem HTTP RFC ).

Zapisana sygnatura czasowa jest częścią barkzasobu i można ją pobrać za pomocą GETdo. Zaktualizowany pies może być również w tym „udokumentowany” GET dogs/X/barks/Y.


4. bark uruchamia polecenie systemowe, aby pobrać najnowszą wersję kodu psa z Githuba. Następnie wysyła wiadomość tekstową z dog.ownerinformacją, że nowy nieśmiertelnik jest w produkcji.

Sformułowanie tego jest skomplikowane, ale jest to w zasadzie proste zadanie asynchroniczne:

POST /v1/dogs/1/barks HTTP/1.1
Host: api.animals.com
Authorization: Basic mAUhhuE08u724bh249a2xaP=

(document body, if needed)

202 Accepted
Location: http://api.animals.com/v1/dogs/1/barks/a65h44

Następnie klient wyda GETS aby /v1/dogs/1/barks/a65h44poznać aktualny stan (jeśli kod został wycofany, to adres e-mail został wysłany do właściciela i takie tam). Za każdym razem, gdy pies się zmienia, obowiązuje a 303.


Podsumowując

Cytując Roya Fieldinga :

Jedyną rzeczą, której REST wymaga od metod, jest to, że są one jednolicie zdefiniowane dla wszystkich zasobów (tj. Aby pośrednicy nie musieli znać typu zasobu, aby zrozumieć znaczenie żądania).

W powyższych przykładach POSTjest jednolicie zaprojektowany. To sprawi, że pies " bark". To nie jest bezpieczne (co oznacza, że ​​kora ma wpływ na zasoby), ani idempotentne (każde żądanie daje nowy bark), co dobrze pasuje do POSTczasownika.

Programista będzie wiedział: a POSTdo barksdaje bark. Kody statusu odpowiedzi (w razie potrzeby również z treścią encji i nagłówkami) wyjaśniają, co się zmieniło i jak klient może i powinien postępować.

Uwaga: Głównymi używanymi źródłami były: książka „ Restful Web Services ”, HTTP RFC i blog Roya Fieldinga .




Edytować:

Pytanie, a tym samym odpowiedź, zmieniły się nieco od czasu ich powstania. Oryginalne pytanie poproszony o projektowaniu URI, takich jak:

ACTION http://api.animals.com/v1/dogs/1/?action=bark

Poniżej znajduje się wyjaśnienie, dlaczego nie jest to dobry wybór:

Sposób, w jaki klienci mówią serwerowi, CO ZROBIĆ z danymi, to informacje o metodzie .

  • Usługi sieciowe RESTful przekazują informacje o metodzie w metodzie HTTP.
  • Typowe usługi w stylu RPC i SOAP przechowują je w treści encji i nagłówku HTTP.

KTÓRA CZĘŚĆ danych [klient chce, aby serwer] działała, jest informacją o zakresie .

  • Usługi RESTful używają identyfikatora URI. Usługi w stylu SOAP / RPC ponownie używają treści encji i nagłówków HTTP.

Jako przykład weźmy identyfikator URI Google http://www.google.com/search?q=DOG. Tam znajdują się informacje o metodzie GETi informacje o zakresie /search?q=DOG.

Krótko mówiąc:

  • W architekturach RESTful informacje o metodzie trafiają do metody HTTP.
  • W architekturach zorientowanych na zasoby informacje o zakresie trafiają do identyfikatora URI.

I praktyczna zasada:

Jeśli metoda HTTP nie pasuje do informacji o metodzie, usługa nie jest zgodna z REST. Jeśli informacje o zakresie nie znajdują się w identyfikatorze URI, usługa nie jest zorientowana na zasoby.

Możesz umieścić akcję „bark” w adresie URL (lub w treści encji) i użyć . Nie ma problemu, to działa i może być najprostszym sposobem na zrobienie tego, ale nie jest to RESTful .POST

Aby Twoja usługa była naprawdę RESTful, być może będziesz musiał cofnąć się o krok i pomyśleć o tym, co naprawdę chcesz tutaj zrobić (jaki wpływ będzie to miało na zasoby).

Nie mogę mówić o twoich konkretnych potrzebach biznesowych, ale pozwól mi podać przykład: Rozważ usługę zamawiania RESTful, w której zamówienia są z identyfikatorami URI example.com/order/123.

Teraz powiedz, że chcemy anulować zamówienie, jak to zrobimy? Można pokusić się o myślenie, że jest to „działanie” „anulowania i zaprojektowanie go jako POST example.com/order/123?do=cancel.

To nie jest RESTful, jak mówiliśmy powyżej. Zamiast tego możemy PUTnową reprezentację elementu orderz canceledelementem wysłanym do true:

PUT /order/123 HTTP/1.1
Content-Type: application/xml

<order id="123">
    <customer id="89987">...</customer>
    <canceled>true</canceled>
    ...
</order>

I to wszystko. Jeśli zamówienia nie można anulować, można zwrócić określony kod statusu. (Dla uproszczenia może być również dostępny projekt zasobów podrzędnych, podobnie jak POST /order/123/canceledw przypadku treści encji true).

W swoim konkretnym scenariuszu możesz spróbować czegoś podobnego. W ten sposób, gdy na przykład szczeka pies, GETat /v1/dogs/1/może zawierać tę informację (np<barking>true</barking> . ) . Lub ... jeśli to zbyt skomplikowane, poluzuj swoje wymagania RESTful i trzymaj sięPOST .

Aktualizacja:

Nie chcę, aby odpowiedź była zbyt obszerna, ale zrozumienie algorytmu ( akcji ) jako zestawu zasobów zajmuje trochę czasu . Zamiast myśleć w kategoriach działań ( „poszukaj miejsc na mapie” ), trzeba myśleć kategoriami wyników tego działania ( „lista miejsc na mapie spełniających kryteria wyszukiwania” ).

Może się okazać, że wrócisz do tego kroku, jeśli okaże się, że projekt nie pasuje do jednolitego interfejsu HTTP.

Zmienne zapytania to informacje o zakresie , ale nie oznaczają nowych zasobów ( /post?lang=enjest to wyraźnie ten sam zasób, co /post?lang=jptylko inna reprezentacja). Są raczej używane do przekazywania stanu klienta (na przykład ?page=10stan ten nie jest przechowywany na serwerze; ?lang=enjest to również przykład) lub parametrów wejściowych do zasobów algorytmicznych ( /search?q=dogs, /dogs?code=1). Ponownie, nie są to odrębne zasoby.

Właściwości (metody) czasowników HTTP:

Inną jasną kwestią, która pojawia się ?action=somethingw identyfikatorze URI, nie jest RESTful, są właściwości czasowników HTTP:

  • GETi HEADsą bezpieczne (i idempotentne);
  • PUTi DELETEsą tylko idempotentni;
  • POST Nie jest.

Bezpieczeństwo : żądanie GETlub HEADżądanie to żądanie odczytania niektórych danych, a nie żądanie zmiany stanu serwera. Klient może poprosić GETlub HEADpoprosić 10 razy i jest to to samo, co zrobienie tego raz lub nigdy nie .

Idempotencja : idempotentna operacja w jednej, która ma ten sam efekt, niezależnie od tego, czy zastosujesz ją raz, czy więcej niż raz (w matematyce mnożenie przez zero jest idempotentne). Jeśli DELETEraz otrzymałeś zasób, ponowne usunięcie będzie miało ten sam efekt (zasób GONEjuż jest ).

POSTnie jest ani bezpieczny, ani idempotentny. Wysłanie dwóch identycznych POSTżądań do zasobu „fabryki” prawdopodobnie spowoduje, że dwa podrzędne zasoby będą zawierały te same informacje. W przypadku przeciążenia (metoda w URI lub treść jednostki) POSTwszystkie zakłady są wyłączone.

Obie te właściwości były ważne dla powodzenia protokołu HTTP (w zawodnych sieciach!): Ile razy aktualizowałeś ( GET) stronę bez czekania, aż zostanie w pełni załadowana?

Utworzenie akcji i umieszczenie jej w adresie URL wyraźnie łamie kontrakt metod HTTP. Po raz kolejny technologia na to pozwala, możesz to zrobić, ale to nie jest projekt RESTful.


Sprzeciwiam się idei, że wywołanie akcji na serwerze, określonej jako akcja w adresie URL, nie jest RESTful. POSTzostał zaprojektowany w celu „dostarczania bloku danych ... do procesu przetwarzania danych” . Wydaje się, że wielu ludzi odróżnia zasoby od działań, ale tak naprawdę działania to tylko rodzaj zasobów.
Jacob Stevens

1
@JacobStevens OP trochę zmienił pytanie, więc muszę zaktualizować moją odpowiedź, aby była bardziej bezpośrednia (sprawdź oryginalne pytanie , może zobaczysz, co mam na myśli). Zgadzam się z POST„dostarczeniem bloku danych ... do procesu przetwarzania danych”, ale różnica polega na tym, że blok danych , a nie blok danych, a procedura (akcja, metoda, polecenie) ma być stracony wtedy. To jest POSTprzeciążenie, a POSTprzeciążanie to projekt w stylu RPC, a nie REST.
acdcjunior

Przypuszczam, że logika akcji / metody byłaby umieszczona na serwerze, w przeciwnym razie jaki byłby cel połączenia? W przypadku, gdy opisujesz, zgadzam się, to nie byłby dobry projekt. Ale metoda lub podprogram, który wykonuje akcję, byłby określony przez URI (co jest kolejnym powodem, dla którego zasób akcji oznaczony jako czasownik na końcu adresu URL jest przydatny i RESTful, chociaż wielu odradza to).
Jacob Stevens

6
Odpowiedź nas zaktualizowana. Jest trochę długi, ponieważ wydawało się, że potrzebne jest dokładne wyjaśnienie („Pamiętaj, że mam podstawowe rozumienie REST”). Trudno było uczynić to tak zrozumiałym, jak to tylko możliwe. Mam nadzieję, że jest to w jakiś sposób przydatne.
acdcjunior

2
Świetne wyjaśnienie, głosowałem, ale nagłówek Location nie powinien być używany w odpowiedzi 202 Accepted. Wydaje się, że to błędna interpretacja, którą wiele osób robi z RFC. Sprawdź ten stackoverflow.com/questions/26199228/…
Delmo

6

I odpowiedział wcześniej , ale ta odpowiedź zaprzecza mój stary odpowiedź i następuje znacznie inną strategię zbliża się do rozwiązania. Pokazuje, jak żądanie HTTP jest zbudowane na podstawie koncepcji definiujących REST i HTTP. Używa również PATCHzamiast POSTlub PUT.

Przechodzi przez ograniczenia REST, następnie składniki HTTP, a następnie możliwe rozwiązanie.

ODPOCZYNEK

REST to zestaw ograniczeń, które mają być zastosowane do rozproszonego systemu hipermediów w celu uczynienia go skalowalnym. Nawet aby nadać temu sens w kontekście zdalnego sterowania akcją, trzeba pomyśleć o zdalnym sterowaniu działaniem jako części rozproszonego systemu hipermedialnego - części systemu służącego do odkrywania, przeglądania i modyfikowania połączonych informacji. Jeśli to więcej kłopotów niż jest to warte, prawdopodobnie nie warto próbować uczynić go RESTful. Jeśli potrzebujesz tylko GUI typu „panel sterowania” na kliencie, który może wyzwalać akcje na serwerze przez port 80, to prawdopodobnie potrzebujesz prostego interfejsu RPC, takiego jak JSON-RPC przez żądania / odpowiedzi HTTP lub WebSocket.

Ale REST to fascynujący sposób myślenia, a przykład w pytaniu jest łatwy do modelowania za pomocą interfejsu RESTful, więc podejmijmy wyzwanie dla zabawy i edukacji.

REST jest definiowany przez cztery ograniczenia interfejsu:

identyfikacja zasobów; manipulowanie zasobami poprzez reprezentacje; komunikaty samoopisowe; oraz hipermedia jako silnik stanu aplikacji.

Pytasz, jak zdefiniować interfejs, spełniający te ograniczenia, za pośrednictwem którego jeden komputer każe drugiemu zrobić szczekanie psa. W szczególności chcesz, aby twój interfejs był HTTP i nie chcesz odrzucać funkcji, które sprawiają, że HTTP REST jest używany zgodnie z przeznaczeniem.

Zacznijmy od pierwszego ograniczenia: identyfikacji zasobów .

Każda informacja, którą można nazwać, może być zasobem: dokument lub obraz, usługa tymczasowa (np. „Dzisiejsza pogoda w Los Angeles”), zbiór innych zasobów, obiekt niewirtualny (np. Osoba) itd. .

Zatem pies jest zasobem. Należy go zidentyfikować.

Mówiąc dokładniej, zasób R jest zmieniającą się w czasie funkcją przynależności M R ( t ), która dla czasu t odwzorowuje zbiór jednostek lub wartości, które są równoważne. Wartości w zestawie mogą być reprezentacjami zasobów i / lub identyfikatorami zasobów .

Ci wymodelować psa poprzez zestaw identyfikatorów i przedstawień i mówiąc wszystkie są powiązane ze sobą w danym czasie. Na razie użyjmy identyfikatora „pies # 1”. To prowadzi nas do drugiego i trzeciego ograniczenia: reprezentacji zasobów i samoopisu .

Komponenty REST wykonują akcje na zasobie, używając reprezentacji do przechwytywania bieżącego lub zamierzonego stanu tego zasobu i przesyłając tę ​​reprezentację między komponentami. Reprezentacja to sekwencja bajtów plus metadane reprezentacji opisujące te bajty.

Poniżej znajduje się sekwencja bajtów przechwytująca zamierzony stan psa, tj. Reprezentacja, którą chcemy skojarzyć z identyfikatorem „pies # 1” (zwróć uwagę, że reprezentuje ona tylko część stanu, ponieważ nie uwzględnia imienia psa, stanu zdrowia lub nawet wcześniejsze szczekanie):

Szczekał co 10 minut od chwili zmiany tego stanu i będzie trwał przez czas nieokreślony.

Ma być dołączony do opisujących go metadanych. Te metadane mogą być przydatne:

To jest angielskie oświadczenie. Opisuje część zamierzonego stanu. Jeśli zostanie odebrany wiele razy, pozwól tylko pierwszemu na efekt.

Na koniec spójrzmy na czwarte ograniczenie: HATEOAS .

REST ... postrzega aplikację jako spójną strukturę informacji i alternatywnych opcji sterowania, za pomocą których użytkownik może wykonać żądane zadanie. Na przykład wyszukiwanie słowa w słowniku online to jedna aplikacja, podobnie jak zwiedzanie wirtualnego muzeum lub przeglądanie zestawu notatek z zajęć w celu przygotowania się do egzaminu. ... Następny stan sterowania aplikacji znajduje się w reprezentacji pierwszego żądanego zasobu, więc uzyskanie tej pierwszej reprezentacji jest priorytetem. ... Aplikacja modelowa jest zatem silnikiem, który przechodzi z jednego stanu do drugiego, badając i wybierając spośród alternatywnych przejść stanów w bieżącym zestawie reprezentacji.

W interfejsie RESTful klient otrzymuje reprezentację zasobów, aby dowiedzieć się, w jaki sposób powinien odebrać lub wysłać reprezentację. Gdzieś w aplikacji musi znajdować się reprezentacja, z której klient może dowiedzieć się, w jaki sposób odebrać lub wysłać wszystkie oświadczenia, które powinien mieć możliwość odebrania lub wysłania, nawet jeśli po łańcuchu oświadczeń dochodzi do tych informacji. Wydaje się to dość proste:

Klient prosi o przedstawienie zasobu zidentyfikowanego jako strona główna; w odpowiedzi otrzymuje reprezentację zawierającą identyfikator każdego psa, którego może chcieć klient. Klient pobiera z niego identyfikator i pyta obsługę, w jaki sposób może wchodzić w interakcję ze zidentyfikowanym psem, a usługa mówi, że klient może wysłać angielskie oświadczenie opisujące część zamierzonego stanu psa. Następnie klient wysyła takie oświadczenie i otrzymuje komunikat o powodzeniu lub komunikat o błędzie.

HTTP

HTTP implementuje ograniczenia REST w następujący sposób:

identyfikacja zasobu : URI

reprezentacja zasobów : treść jednostki

opis własny : metoda lub kod statusu, nagłówki i ewentualnie części ciała jednostki (np. URI schematu XML)

HATEOAS : hiperłącza

Zdecydowałeś się http://api.animals.com/v1/dogs/1 na URI. Załóżmy, że klient uzyskał to z jakiejś strony w witrynie.

Wykorzystajmy to ciało encji (wartość nextto znacznik czasu; wartość 0oznacza „kiedy to żądanie zostanie odebrane”):

{"barks": {"next": 0, "frequency": 10}}

Teraz potrzebujemy metody. PATCH pasuje do opisu „części zamierzonego stanu”, na który się zdecydowaliśmy:

Metoda PATCH żąda, aby zestaw zmian opisanych w jednostce żądania został zastosowany do zasobu zidentyfikowanego przez identyfikator URI żądania.

I kilka nagłówków:

Aby wskazać język ciała encji: Content-Type: application/json

Aby upewnić się, że zdarzy się to tylko raz: If-Unmodified-Since: <date/time this was first sent>

Mamy prośbę:

PATCH /v1/dogs/1/ HTTP/1.1
Host: api.animals.com
Content-Type: application/json
If-Unmodified-Since: <date/time this was first sent>
[other headers]

{"barks": {"next": 0, "frequency": 10}}

Po pomyślnym zakończeniu klient powinien otrzymać 204w odpowiedzi kod statusu lub, 205jeśli reprezentacja/v1/dogs/1/ zmieniła się, aby odzwierciedlić nowy harmonogram szczekania.

W przypadku niepowodzenia powinien otrzymać plik 403 pomocny komunikat dlaczego.

REST nie jest niezbędny, aby usługa odzwierciedlała harmonogram korygowania w reprezentacji w odpowiedzi GET /v1/dogs/1/ , ale najbardziej sensowne byłoby, gdyby reprezentacja JSON obejmowała to:

"barks": {
    "previous": [x_1, x_2, ..., x_n],
    "next": x_n,
    "frequency": 10
}

Traktuj zadanie cron jako szczegół implementacji, który serwer ukrywa przed interfejsem. Na tym polega piękno ogólnego interfejsu. Klient nie musi wiedzieć, co serwer robi za kulisami; liczy się tylko to, że usługa rozumie i reaguje na żądane zmiany stanu.


3

Większość ludzi używa POST do tego celu . Jest on odpowiedni do wykonywania „wszelkich niebezpiecznych lub nieefektywnych operacji, gdy żadna inna metoda HTTP nie wydaje się odpowiednia”.

Interfejsy API, takie jak XMLRPC, używają POST do wyzwalania akcji, które mogą uruchamiać dowolny kod. „Działanie” jest zawarte w danych POST:

POST /RPC2 HTTP/1.0
User-Agent: Frontier/5.1.2 (WinNT)
Host: betty.userland.com
Content-Type: text/xml
Content-length: 181

<?xml version="1.0"?>
<methodCall>
   <methodName>examples.getStateName</methodName>
   <params>
      <param>
         <value><i4>41</i4></value>
         </param>
      </params>
   </methodCall>

Podano przykład RPC, aby pokazać, że POST jest konwencjonalnym wyborem czasowników HTTP dla metod po stronie serwera. Oto przemyślenia Roya Fieldinga na temat POST - prawie mówi, że użycie metod HTTP zgodnie z opisem jest RESTful.

Zauważ, że samo RPC nie jest bardzo RESTful, ponieważ nie jest zorientowane na zasoby. Ale jeśli potrzebujesz bezpaństwowości, buforowania lub warstwowania, nie jest trudno dokonać odpowiednich transformacji. Na przykład patrz http://blog.perfectapi.com/2012/opinionated-rpc-apis-vs-restful-apis/ .


Myślę, że zakodowałbyś URL-a parametry nie umieszczając go w ciągu zapytania
tacos_tacos_tacos

@Kirk Tak, ale z jedną drobną modyfikacją upuść ostatni ukośnik: POST api.animals.com/v1/dogs1?action=bark
Raymond Hettinger

Jeśli zastosujesz się do porady w tej odpowiedzi, pamiętaj, że wynikowy interfejs API nie będzie zgodny z REST.
Nicholas Shanks

2
To nie jest RESTful, ponieważ HTTP ustanawia adres URL jako identyfikator zasobu, a adres URL /RPC2nie robi nic w celu zidentyfikowania zasobu - identyfikuje technologię serwera. Zamiast tego methodNamepróbuje „zidentyfikować” „zasób” - ale nawet wtedy nie korzysta z rozróżnienia rzeczownik / czasownik; jedyną rzeczą podobną do „czasownika” jest tutaj methodCall. To jest jak „pobieranie nazwy stanu” zamiast „pobieranie nazwy stanu” - to drugie ma o wiele więcej sensu.
Jordan

+1 dla linków; bardzo pouczające, a „uparty eksperyment RPC” jest pomysłowy.
Jordania

2

POSTto metoda HTTP zaprojektowana dla

Dostarczenie bloku danych ... do procesu przetwarzania danych

Metody po stronie serwera obsługujące akcje niezamapowane na CRUD są tym, co Roy Fielding zamierzał w REST, więc jesteś w tym dobry i dlatego POSTzostał stworzony, aby nie był idempotentny. POSTobsłuży większość wysyłania danych do metod po stronie serwera w celu przetwarzania informacji.

To powiedziawszy, w scenariuszu szczekania psa, jeśli chcesz, aby szczekanie po stronie serwera było wykonywane co 10 minut, ale z jakiegoś powodu potrzebujesz wyzwalacza pochodzącego od klienta, PUT lepiej służyłoby temu celowi ze względu na jego idempotencję. Cóż, ściśle według tego scenariusza nie ma wyraźnego ryzyka, że ​​wiele żądań POST spowoduje zamiast tego miauczenie twojego psa, ale tak czy inaczej, to jest celem dwóch podobnych metod. Moja odpowiedź na podobne pytanie SO może być dla Ciebie przydatna.


1
PUT vs. POST dotyczy adresu URL. Trzeci akapit po 9.6 PUT mówi, że celem tych dwóch metod jest to, że PUTadres URL odnosi się do tego, co powinno zostać zastąpione treścią klienta, a POSTadres URL odnosi się do tego, co powinno przetwarzać zawartość klienta, jak chce.
Jordania

1

Jeśli założymy, że szczekanie jest zasobem wewnętrznym / zależnym / podrzędnym, na którym konsument może działać, możemy powiedzieć:

POST http://api.animals.com/v1/dogs/1/bark

pies numer 1 szczeka

GET http://api.animals.com/v1/dogs/1/bark

zwraca ostatni znacznik czasu kory

DELETE http://api.animals.com/v1/dogs/1/bark

nie dotyczy! więc zignoruj ​​to.


Jest to tylko RESTful, jeśli uważasz, że /v1/dogs/1/barkjest to zasób per se i POSTma być opisem tego, jak powinien zmienić się stan wewnętrzny tego zasobu. Uważam, że bardziej sensowne jest rozważenie tego /v1/dogs/1/jako zasobu i wskazanie w ciele istoty, że powinno szczekać.
Jordan,

mmm ... no cóż, to zasób, którego stan możesz zmienić. Ponieważ rezultatem zmiany jego stanu jest hałas, nie oznacza to, że jest mniej zasobów! Patrzysz na Bark jako na czasownik (czyli), dlatego nie możesz uznać go za zasób. Patrzę na to jako na zasób zależny, którego stan można zmienić, a ponieważ jego stan jest logiczny, nie widzę powodu, aby wspominać o nim w ciele jednostki. To tylko moja opinia.
bolbol

1

Wcześniejsze wersje niektórych odpowiedzi sugerowały użycie RPC. Nie trzeba patrzeć na RPC, ponieważ jest całkiem możliwe, aby robić to, co chcesz jednoczesnym przestrzeganiu ograniczeń resztę.

Po pierwsze, nie umieszczaj parametrów akcji w adresie URL. Adres URL określa, do czego wykonujesz akcję, a parametry zapytania są częścią adresu URL. Powinien być traktowany w całości jako rzeczownik. http://api.animals.com/v1/dogs/1/?action=barkto inny zasób - inny rzeczownik - to http://api.animals.com/v1/dogs/1/. [nb Asker usunął ?action=barkidentyfikator URI z pytania.] Na przykład porównaj http://api.animals.com/v1/dogs/?id=1z http://api.animals.com/v1/dogs/?id=2. Różne zasoby, rozróżniane tylko na podstawie ciągu zapytania. Zatem akcja żądania, chyba że bezpośrednio odpowiada bezcielesnemu typowi metody (TRACE, OPTIONS, HEAD, GET, DELETE, itp.) Musi być zdefiniowana w treści żądania.

Następnie zdecyduj, czy działanie jest „ idempotentne ”, co oznacza, że ​​można je powtórzyć bez negatywnych skutków (więcej wyjaśnień znajduje się w następnym akapicie). Na przykład ustawienie wartości na true może zostać powtórzone, jeśli klient nie jest pewien, czy nastąpił pożądany efekt. Wysyłają żądanie ponownie, a wartość pozostaje prawdziwa. Dodanie 1 do liczby nie jest idempotentne. Jeśli klient wysyła polecenie Add1, nie jest pewien, czy zadziałało, i wysyła je ponownie, czy serwer dodał jedną lub dwie? Kiedy już to ustalisz, będziesz mieć lepszą pozycję do wyboru pomiędzy PUTi POSTdla swojej metody.

Idempotentne oznacza, że ​​żądanie można powtórzyć bez zmiany wyniku. Efekty te nie obejmują logowania i innych podobnych działań administratora serwera. Korzystając z pierwszego i drugiego przykładu, wysłanie dwóch e-maili do tej samej osoby skutkuje innym stanem niż wysłanie jednego e-maila (odbiorca ma dwa w swojej skrzynce odbiorczej, które mogą uznać za spam), więc zdecydowanie użyłbym do tego POST . Jeśli barkCount w przykładzie 2 ma być widziany przez użytkownika twojego API lub wpływa na coś, co jest widoczne dla klienta, jest to również coś, co sprawi, że żądanie nie będzie idempotentne. Jeśli ma być przeglądany tylko przez Ciebie, liczy się jako logowanie do serwera i powinien być ignorowany podczas określania idempotencji.

Na koniec określ, czy można oczekiwać, że akcja, którą chcesz wykonać, zakończy się natychmiastowym sukcesem, czy nie. BarkDog to szybko zaliczająca się akcja. RunMarathon nie jest. Jeśli twoja akcja jest powolna, rozważ zwrócenie a 202 Accepted, z adresem URL w treści odpowiedzi, aby użytkownik mógł odpytać, czy akcja została zakończona. Alternatywnie, poproś użytkowników POST do adresu URL listy, /marathons-in-progress/a następnie po zakończeniu akcji przekieruj ich z adresu URL identyfikatora w toku na adres /marathons-complete/URL.
W szczególnych przypadkach # 1 i # 2 serwer powinien hostować kolejkę, a klient wysyłał do niej partie adresów. Akcją nie byłoby SendEmails, ale coś w rodzaju AddToDispatchQueue. Serwer może następnie odpytać kolejkę, aby sprawdzić, czy są jakieś oczekujące adresy e-mail i wysłać e-maile, jeśli je znajdzie. Następnie aktualizuje kolejkę, aby wskazać, że oczekująca akcja została wykonana. Miałbyś inny identyfikator URI pokazujący klientowi bieżący stan kolejki. Aby uniknąć podwójnego wysyłania wiadomości e-mail, serwer może również przechowywać dziennik, do którego wysłał tę wiadomość e-mail i sprawdzać każdy adres, aby upewnić się, że nigdy nie wyśle ​​dwóch na ten sam adres, nawet jeśli POSTAWASZ tę samą listę dwa razy do kolejka.

Wybierając identyfikator URI do czegokolwiek, staraj się myśleć o nim jako wyniku, a nie akcji. Na przykład google.com/search?q=dogspokazuje wyniki wyszukiwania słowa „psy”. Nie ma konieczności wykonywania wyszukiwania.

Przypadki # 3 i # 4 z Twojej listy również nie są działaniami idempotentnymi. Sugerujesz, że różne sugerowane efekty mogą wpłynąć na projekt interfejsu API. We wszystkich czterech przypadkach użyłbym tego samego interfejsu API, ponieważ wszystkie cztery zmieniają „stan świata”.


Powiedzmy, że chodzi o przerzucenie gigantycznej kolejki e-maili i wysłanie wiadomości do grupy osób. Czy to idempotentne? Czy akcje są idempotentne dla PUT czy POST?
Kirk Ouimet

@kirk Rozszerzyłem moją odpowiedź.
Nicholas Shanks

0

Zobacz moją nową odpowiedź - zaprzecza tej i wyjaśnia REST i HTTP jaśniej i dokładniej.

Oto zalecenie, które jest RESTful, ale z pewnością nie jest jedyną opcją. Aby rozpocząć szczekanie, gdy usługa otrzyma żądanie:

POST /v1/dogs/1/bark-schedule HTTP/1.1
...
{"token": 12345, "next": 0, "frequency": 10}

token to dowolna liczba, która zapobiega zbędnym szczekaniu bez względu na to, ile razy to żądanie jest wysyłane.

nextwskazuje czas następnej kory; wartość 0oznacza „JAK NAJSZYBCIEJ”.

Zawsze GET /v1/dogs/1/bark-schedulepowinieneś dostać coś takiego, gdzie t to czas ostatniego szczekania i U jest t + 10 minut:

{"last": t, "next": u}

Zdecydowanie zalecam użycie tego samego adresu URL do zażądania szczekania, którego używasz do sprawdzenia aktualnego stanu szczekania psa. Nie jest to niezbędne dla REST, ale podkreśla akt modyfikowania harmonogramu.

Odpowiedni kod statusu to prawdopodobnie 205 . Wyobrażam sobie klienta, który patrzy na bieżący harmonogram POSTpod ten sam adres URL, aby go zmienić, i jest instruowany przez usługę, aby ponownie spojrzał na harmonogram, aby udowodnić, że został zmieniony.

Wyjaśnienie

ODPOCZYNEK

Zapomnij na chwilę o HTTP. Ważne jest, aby zrozumieć, że zasób to funkcja, która wymaga czasu jako danych wejściowych i zwraca zestaw zawierający identyfikatory i reprezentacje . Uprośćmy to, aby: zasób to zbiór R identyfikatorów i reprezentacji; R może się zmieniać - członków można dodawać, usuwać lub modyfikować. (Chociaż to zły, niestabilny projekt usuwania lub modyfikowania identyfikatorów.) Mówimy, że identyfikator będący elementem R identyfikuje R , a reprezentacja będąca elementem R oznacza R .

Powiedzmy, że R to pies. Zdarza Ci się zidentyfikować R jako /v1/dogs/1. (Czyli /v1/dogs/1jest członkiem R ). To tylko jeden z wielu sposobów można zidentyfikować R . Możesz również zidentyfikować R jako /v1/dogs/1/x-raysi jako /v1/rufus.

Jak reprezentujesz R ? Może ze zdjęciem. Może z zestawem promieni rentgenowskich. A może ze wskazaniem daty i godziny ostatniego szczekania R. Pamiętaj jednak, że są to wszystkie reprezentacje tego samego zasobu . /v1/dogs/1/x-raysjest identyfikatorem tego samego zasobu, który jest reprezentowany przez odpowiedź na pytanie "kiedy R ostatnio szczekał?"

HTTP

Wielokrotne reprezentacje zasobu nie są zbyt przydatne, jeśli nie możesz odnieść się do tego, który chcesz. Dlatego HTTP jest użyteczny: pozwala łączyć identyfikatory z reprezentacjami . Oznacza to, że jest to sposób, aby usługa otrzymała adres URL i zdecydowała, która reprezentacja ma być dostarczona klientowi.

A przynajmniej tak jest GET. PUTjest w zasadzie odwrotnością GET: you PUTa reprezentacja r pod adresem URL, jeśli chcesz, aby przyszłe GETżądania do tego adresu URL zwracały r , z pewnymi możliwymi tłumaczeniami, takimi jak JSON na HTML.

POSTto luźniejszy sposób modyfikowania reprezentacji. Pomyśl o logice wyświetlania i logice modyfikacji, które są sobie odpowiednikami - obie odpowiadają temu samemu adresowi URL. Żądanie POST to żądanie logiki modyfikacji w celu przetworzenia informacji i zmodyfikowania wszelkich reprezentacji (nie tylko reprezentacji znajdujących się pod tym samym adresem URL), jakie usługa uzna za stosowne. Zwróć uwagę na trzeci akapit po 9.6 PUT : nie zastępujesz rzeczy pod adresem URL nową treścią; prosisz rzecz pod adresem URL o przetworzenie pewnych informacji i inteligentną odpowiedź w formie reprezentacji informacyjnych.

W naszym przypadku prosimy o logikę modyfikacji w /v1/dogs/1/bark-schedule (która jest odpowiednikiem logiki wyświetlania, która mówi nam, kiedy ostatnio szczekał i kiedy będzie ponownie szczekał) o przetworzenie naszych informacji i odpowiednie zmodyfikowanie niektórych reprezentacji. W odpowiedzi na przyszłe GETbłędy logika wyświetlania odpowiadająca temu samemu adresowi URL powie nam, że pies szczeka teraz tak, jak chcemy.

Potraktuj zadanie crona jako szczegół implementacji. HTTP zajmuje się przeglądaniem i modyfikowaniem reprezentacji. Od tej chwili obsługa poinformuje klienta, kiedy ostatnio szczekał pies i kiedy będzie następny. Z punktu widzenia usługi jest to uczciwe, ponieważ te czasy odpowiadają przeszłym i planowanym zadaniom crona.


-1

REST jest standardem zorientowanym na zasoby i nie jest oparty na działaniu, jak byłoby to RPC.

Jeśli chcesz, aby Twój serwer szczekał , powinieneś przyjrzeć się różnym pomysłom, takim jak JSON-RPC lub komunikację w gniazdach internetowych.

Moim zdaniem każda próba zachowania RESTful zakończy się niepowodzeniem: możesz wydać POSTz actionparametrem, nie tworzysz żadnych nowych zasobów, ale ponieważ możesz mieć skutki uboczne, jesteś bezpieczniejszy.


POSTzostał zaprojektowany w celu „dostarczania bloku danych… do procesu przetwarzania danych” . Wydaje się, że wielu ludzi odróżnia zasoby od działań, ale tak naprawdę działania to tylko rodzaj zasobów. Wywołanie zasobu akcji na serwerze jest nadal jednolitym interfejsem, buforowalnym, modułowym i skalowalnym. Jest również bezstanowy, ale może zostać naruszony, jeśli klient ma oczekiwać odpowiedzi. Ale wywołanie „metody void” na serwerze jest tym, co zamierzał Roy Fielding w przypadku REST .
Jacob Stevens

Jak opisuję w mojej odpowiedzi , możesz w REST niejawnie spowodować wykonanie akcji przez serwer, prosząc go o powiedzenie od tej pory „Twoja akcja została zakończona”, podczas gdy RPC opiera się na pomyśle po prostu poprosić serwer o wykonanie akcja. Oba mają sens, podobnie jak programowanie imperatywne i deklaratywne mają sens.
Jordan
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.