W kilku spośród moich poprzednich projektów miałem okazję implementować prawdziwie RESTowe API. Przez “prawdziwie” RESTowe mam na myśli takie, które chciało się plasować na trzecim poziom Richardson Maturity Model. Po tych projektach, zakochałem się w koncepcji HATEOAS i zacząłem stosować w innych projektach (czasem nawet jeżeli API nie było RESTowe w ogóle).


Mateusz Stasch. Full-stack Developer w firmie Future Processing. Od kilku lat jest konsultantem technicznym oraz architektem, pomagającym developerom i klientom w rozwiązywaniu ich największych problemów technologicznych. Na co dzień pisze (głównie) w C#, JavaScript oraz F#. Wielki fan Domain Driven Design, Vima oraz filozofii Unixa.


W przykładach będę używał JSON-a jako formy reprezentacji danych, ale metoda serializacji danych, jest tutaj wtórna. Tak po prawdzie, to linki w XML-u są nawet ładniejsze.

Czym jest HATEOAS?

Hypermedia As The Engine Of Application State — jeden z “constrainów” aplikacji tworzonych w architekturze REST. Mówi on, że aplikacja powinna pozwalać na sterowanie poprzez dodatkowe informacje pozwalające na poruszanie się po API RESTowym — linki (hypermedia) do zasobów.

W moim odczuciu, to jest właśnie ta rzecz, która odróżnia RESTa od reszty świata. W podejściu RPC, musisz dokładnie wiedzieć, gdzie jest twoje API, jak nazywają się metody i jak dokładnie je wywołać. W teoretycznym RESTcie, mógłbyś mieć tylko jeden endpoint do wysyłania zapytania, które pokieruje Cię dalej w głąb aplikacji do następnych zasobów. W ten sam sposób, wszystkie następne zapytania, powinny zwracać listę odnośników do następnych zasobów.

Przykładowa aplikacja implementująca HATEOAS mogłaby zwrócić następującą odpowiedź:

W przykładzie aplikacja informuje nas, że klient ma dostęp do pobrania dwóch zasobów orders i authors. Otrzymany JSON ma specjalne pole _links, które posiada odnośniki do tych zasobów. Każdy link ma podobną strukturę:

  • rel — nazwa zasobu/relacji,
  • href — URI do zasobu,
  • method — metoda HTTP jakiej klient powinien użyć aby przejść do tej relacji.

Pierwsze dwa są znane z tagu <a> w HTMLu, który (jeżeli użyty poprawnie) jest bardzo koncepcyjnie zbliżony do tego co chcemy osiągnąć poprzez HATEOAS. Trzeci to podpowiedź, dla kodu klienckiego, w jaki sposób tworzyć zapytania do API.

Kiedy odpytamy, główny endpoint naszej aplikacji, może ona odpowiedzieć (jak w przykładzie) z listą wszystkich obsługiwanych przez nią zasobów. Jeżeli adresy tych zasobów będą się w czasie zmieniać, to o ile utrzymamy tę samą nazwę relacji to nasze aplikacje klienckie nie będą musiały być rekonfigurowane po każdej takiej zmianie.

Co istotne, generowane hypermedia są ściśle kontekstowe, zatem jeżeli zapytamy serwer z uprawnieniami administracyjnymi, moglibyśmy (w odróżnieniu od standardowych użytkowników) otrzymać dodatkowy zasób:

Taki rodzaj odpowiedzi, jest łatwy do interpretacji i wykorzystania w aplikacjach klienckich. Tego typu API są tworzone jednak raczej do wykorzystania przez komputery, a nie ludzi. Zobaczmy zatem co możemy zrobić z tak zbudowanym API.

Hmm, jak mogę to wykorzystać?

Poniżej, przedstawię dwa przykłady wykorzystania, które często powtarzały się w moich poprzednich projektach. Jeden to typowy przypadek maszyna-maszyna, drugi odpowiada bardziej za interakcję z użytkownikiem.

The Crawler

Pierwszy projekt, w którym miałem okazję pracować z HATEOAS (i z REST w ogóle) to była olbrzymia platforma dla aplikacji zbudowanych w filozofii REST (true REST) pisana od podstaw w Node.js (to nie była moja decyzja). Byłem osobiście zaangażowany w napisanie całej “maszynerii” odpowiedzialnej za HATEOAS (generowanie linków, badanie stanu apki, biblioteki klienckie, itp.). Na początkowym etapie projektu miałem poczucie, że HATEOAS to taki przerost formy nad treścią, ani to przyjemne do pisania, ani praktyczne. Na całe szczęście, następnie moim zadaniem stało się napisanie jednego z kluczowych komponentów tej platformy, komponentu który garściami czerpał z dobrodziejstw jakie dawał HATEOAS.

Nasz problem był następujący: potrzebowaliśmy efektywnego mechanizmu wyszukiwania, do tego zadania wybraliśmy ElasticSearch. Niestety, ES nie był naszym głównym magazynem danych, trzeba go było zasilić każdą zmienioną encją. Naturalnym rozwiązaniem, które od razu przychodzi do głowy, jest włożenie pomiędzy główną aplikację a ES jakąś kolejkę i gotowe. Jednak, z powodów, które były dobrze uzasadnione (temat na innego posta), nie mogliśmy w całej architekturze wykorzystywać żadnych kolejek. Mieliśmy użyć pollingu danych — ten pomysł, był sprawdzony w boju przez głównego architekta tego projektu i zdecydowanie się sprawdził, dając pewne benefity, które nie mają kolejki.

Głównym komponentem, który miał wykonywać pracę był Crawler. Pomysł był prosty.

1. Odpytaj endpoint do synchronizacji.

2. Usługi odpowiadają stroną ostatnio zmienionych zasobów, oraz linkiem do następnej strony.

Może się zdarzyć, że usługa odpowie pustą stroną — jest to zachowanie oczekiwane, w takiej sytuacji możemy odczekać pewien okres i wznowić odpytywanie.

3. Wrzuć pobrane dane do ElasticSearch.

4. Idź do punktu 1.

Crawler, przechowywał aktualne linki do stron, na których operował, tak aby w razie restartu usługi mógł wrócić do przetwarzania od tego samego miejsca, w którym skończył.

Takie podejście może się wydawać przekombinowane, ale otwiera możliwości oczekiwanych przez platformę, którą tworzyliśmy:

1. Inni klienci naszej platformy (nieznani na chwilę odpalania platformy), mogą pisać własne crawlery przeznaczone do innych celów — mają dostęp do wszystkich danych, wystarczy zacząć od pustego afterId.

2. Istniejące crawlery, w razie poważnych zmian, mogą być zrestartowane i zaczynać przerzucanie danych od początku.

3. Co istotne, crawler był komponentem generycznym. Kawałek kodu odpowiedzialny za wrzucanie danych do ElasticSearcha był całkowicie ortogonalny do kodu, który przeglądał (w bezpieczny sposób) strony z zasoby. W efekcie, ten kod był wykorzystany w dziesiątkach komponentów. Przeglądanie stron, było nawet użyte nieśmiale w testach integracyjnych systemu.

4. Dzięki temu, że Crawler wykorzystuje standardowe mechanizmy HTTP, może korzystać z wszystkich ich benefitów. Przeniesienie usługi w inne miejsce na serwerze, nie wymaga nawet restartu Crawlera, wystarczy przez jakiś czas zwracać Location: <nowy_adres> w nagłówkach odpowiedzi z miejsca, gdzie do tej pory stała usługa. Itp. Itd.

Pamiętajcie, że prawdziwa implementacja była znacznie bardziej skomplikowana niż ogólny pomysł przedstawiony tutaj, używała m.in: równoległego pobierania zasobów, transformacji zasobów i cachowania, aby uzyskać maksymalną wydajność.

UI sterowane hypermediami

Poprzedni przykład, może wydawać się dość egzotyczny, wszak nie wszyscy w ten sposób przepychamy dane w systemie. Ale przykład, które teraz zaprezentuję, powinien wydać się znajomy każdemu, kto robił kiedykolwiek UI do jakiegokolwiek systemu. Wyobraź sobie, że piszesz system do zarządzania dokumentami, twoje zadanie to dodać przycisk [USUŃ] do formatki z podglądem dokumentu. Ten przycisk powinien być aktywny tylko w pewnych sytuacjach, np. dokument nie jest skasowany (możesz przeglądać usunięte dokumenty — bo czemu nie, to realne wymaganie) oraz użytkownik musi mieć prawa do tego usuwania dokumentów. Jaka byłaby pierwsza, naiwna implementacja po stronie interfejsu użytkownika? Prawdopodobnie taka:

Możliwe, że ten kod byłby ukryty za lepszą abstrakcją, ale ciągle by tam był. Nie mieliście nigdy poczucia, że pisanie takiego kodu to mijanie się z celem? Ten kod jest bardzo podatny na błędy. Jeżeli pojawi się wymaganie, aby zmienić logikę decydującą czy ten przycisk powinien być aktywny to programista, który będzie implementował tę zmianę będzie odpowiedzialny za znalezienie wszystkich miejsc w aplikacji, które robią podobną logikę i zmienienie ich. Wystarczy zmienić logikę inaczej w jednym z tych miejsc i błąd gotowy. To jest kosztowny sposób tworzenia aplikacji, który na pierwszy rzut oka wydaje się być tani. A co gdyby klienci nie byli pod naszą kontrolą? Lepiej o tym nawet nie myśleć, dużo maili…

To jest coś co widziałem, w wielu projektach, ale zawsze miałem poczucie, że jest z tym coś nie tak. Musi istnieć lepsza opcja. Nie zaskoczę was, pisząc, że jest to HATEOAS. Używanie linków do określania czy akcja może być wykonana, czy nie znacznie upraszcza pisanie klientów. Taki kod mógłby wyglądać następująco:

Pewnie da się to zrobić jeszcze czytelniej, lepszymi abstrakcjami. Co ciekawe, w tym przykładzie ręczne aktywowanie tego przycisku przez nieprzyjaznego użytkownika, ciągle nie pozwoli na próbę wykonania tej akcji, bo nie wiadomo nawet pod jaki adres wysłać zapytanie.
Nie fajnie byłoby pisać mniej kodu, jednocześnie dostarczając to samo? Dodatkowo bardziej odpornego na błędy? No chyba fajnie! How cool is that?

Ciągle powinieneś jednak sprawdzić, czy akcja jest “legalna” po stronie serwera, ale ta operacja powinna być relatywnie prosta, używając tego samego kodu, który wygenerował linka do akcji.

Podsumowanie

Jak mogłeś się przekonać, HATEOAS potrafi usunąć sporo logiki biznesowej, które była zduplikowana w aplikacjach klienckich oraz uczynić klientów mniej czułych na zmiany po stronie backendu.

Bibliografia:


Artykuł został pierwotnie opublikowany na blogu mattstasch.net. Zdjęcie główne artykułu pochodzi z negativespace.co.

Zapraszamy do dyskusji
Nie ma więcej wpisów

Send this to a friend