Backend, Frontend

HATEOAS czy Hate All of Us?

Na wstępie powiem, że cały artykuł to moja prywatna opinia. Chciałbym włączyć się do dyskusji na podstawie własnych doświadczeń. Przez całe moje życie backend pisany w Spring (Java) omijał mnie szerokim łukiem. Nastał jednak czas, kiedy przyszło mi skonsumować API pisane w Spring. Programista backendowy zdecydował się na HATEOAS jako fundament REST’a. Jak mi poszło?


Mariusz Walczak. Tech lead w Softfin. Absolwent Warszawskiej Wyższej Szkoły Informatycznej. Pasjonat inżynierii oprogramowania, swoje aplikacje tworzy w PHP i językach opartych na ES6/7. Prywatnie miłośnik futrzanych czworonogów, oraz winiarstwa i nalewkarstwa. Wszystkie jego teksty znajdziecie pod tym adresem.


Czym jest ten potwór?

Pan Richardson opublikował kiedyś poziomy dojrzałości API, według których mamy trzy poziomy, ale liczone po programistycznemu, czyli od zera. W skrócie:

Poziom zero to API, które nie ma ustalonej architektury. To raczej bagienko, przez które musimy przejść, coś znaleźć i może uda nam się coś tam odpytać.

Poziom jeden to w pełni znormalizowana architektura, ale w której nasze endpointy ograniczają się do GET/POST. Mamy tutaj już identyfikator URI i unikalny adres URL.

Poziom dwa to rozszerzenie poziomu jeden o wszystkie rodzaje żądań, czyli POST/GET/PUT/DELETE itd. Na tym etapie mówimy już o API typu RESTfull, dodajmy do tego, że stosujemy zunifikowane kody błędów. Ogólnie w pełni RESTfull API.

Poziom trzy to RESTfull API + HATEOAS, czyli standard jak powinny wyglądać odpowiedzi, aby klient mógł sobie swobodnie biegać po API.

Pokażmy jednak strukturę i chwilkę się nad nią pochylmy.

{

  "_embedded": {

    "customerList": [{

        "customerId": "10A",

        "customerName": "Jane",

        "companyName": "ABC Company",

        "_links": {

          "self": {

            "href": "http://localhost:8080/spring-security-rest/api/customers/10A"

          },

          "allOrders": {

            "href": "http://localhost:8080/spring-security-rest/api/customers/10A/orders"

          }

        }

      },{

        "customerId": "20B",

        "customerName": "Bob",

        "companyName": "XYZ Company",

        "_links": {

          "self": {

            "href": "http://localhost:8080/spring-security-rest/api/customers/20B"

          },

          "allOrders": {

            "href": "http://localhost:8080/spring-security-rest/api/customers/20B/orders"

          }

        }

      },{

        "customerId": "30C",

        "customerName": "Tim",

        "companyName": "CKV Company",

        "_links": {

          "self": {

            "href": "http://localhost:8080/spring-security-rest/api/customers/30C"

          }

        }

      }]

  },

  "_links": {

    "self": {

      "href": "http://localhost:8080/spring-security-rest/api/customers"

    }

  }

}

Źródło https://www.baeldung.com/spring-hateoas-tutorial

Bezsensowne linki

Widzimy tutaj na najwyższy poziomie _embeded, _links. W pierwszym mamy naszą odpowiedź, a w drugim link do tego żądania, który jest moim zdaniem niepotrzebny. Skoro odpytałem ten link, to po co mi on w zwrotce? No nic, pójdźmy dalej.

Wchodzimy w konkretną odpowiedź, a tam znowu linki. Zauważcie, że każda relacja oraz wszystko to co jest związane z tym obiektem, mamy nie w formie id, czy obiektu, tylko w formie linku do zasobu.

Żeby znaleźć plusy takiego rozwiązania wystarczy odpytać API, żeby wiedzieć co można zrobić. Dodatkowo na tej podstawie można uruchomić HAL i zrobić interfejs graficzny do biegania po API oraz generowania zapytań. Wszystko pięknie, prawda?

Do tego marzenia dorzućmy jeszcze autoimplementację w Spring przy dobrze zrobionym projekcie. Wystarczy tylko utworzyć Domain Objecty, JPA i kilka innych drobnostek i samo się robi (przepraszam za ogólniki, ale jestem PHP, NodeJS, a nie Javowcem). Krótko mówiąc: super rozwiązanie. Nie dość, że mamy automatykę w wypluwaniu API, wszystko idealnie opisane dla używających, to jeszcze mamy interfejs graficzny, gdzie można sobie to wszystko przejrzeć, obejrzeć, potestować. No super.

Warto wspomnieć, że możemy dodawać projekcje, czyli specjalny parametr, który pozwala na wygenerowanie specjalnego widoku z dodatkowymi polami. No bajka.

Zbyt dużo zapytań

Wtedy przychodzi osoba, która chce to API pożreć. Pierwsze chwile są nawet miłe, niby wszystko fajnie opisane, wiem gdzie szukać tego czego potrzebuję. Szybko one mijają i czar pryska. Po pierwsze, w przypadku poziomu 2, jeśli mamy ogarniętego backendowca, to okazuje się, że jedno zapytanie i mamy co chcemy w jednej odpowiedzi. Tutaj są te projekcje, ale nie zawsze, często trzeba pobrać użytkownika, potem pobrać jego uprawnienia, potem pobrać jego artykuły, potem pobrać kategorię w jakich piszę. W projekcie w jakim to zastałem, żeby wyświetlić listę z podstawowymi danymi, musieliśmy robić 8 żądań, 8 asynchronicznych żądań. Za każdym razem odpytując API o coś tam.

W pewnym momencie nawet znaleźliśmy taką czynność, która jednym kliknięciem generuje 12 zapytań do API. W ciągu minuty mogłem więc wygenerować takich kliknięć około 20, czyli jedna osoba może atakować API 240 razy na minutę. Kolejna sprawa, jak już sobie pobraliśmy to wszystko to po stronie frontendu musieliśmy to poskładać do obiektów jakich potrzebujemy. Dostaliśmy więc puzzle do zabawy.

W przypadku RESTfull, to backend poprzez odpowiednie zapytanie może uzyskać obiekt jaki jest potrzebny, a potem frontend dostając go, od razu może sobie wrzuć go do obiektu i wyświetlić. Co więcej nie jest to bardzo kosztowne, bo to tylko zapytanie poprzez ORM. W przypadku frontendu, to scalanie kilku obiektów w jeden i często wyrzucanie zbyt dużej ilości danych do widoku, bo nie wszystko pokazujemy, natomiast stworzenie jeszcze funkcji czyszczących z niechcianych parametrów, lub odpowiednich mapperów, to często zbyt dużo pracy. W tym przypadku na frontend spadło mapowanie i tworzenie obiektów takie jakie są potrzebne, wrzucanie tego w całość, potem podział na obiekty jakie są potrzebne. Podsumowując, jak tworzę nowy element relacyjny, to muszę utworzyć ten obiekt poprzez odpytanie API, a następnie stworzyć link i tak nadać relacje.

Te linki w pewnym momencie stały się utrapieniem. 30% aplikacji było tworzeniem obiektów, linków i walidacja, aby jakoś to wszystko wyglądało. Pozostałe 70% to było to, co mamy wyświetlać. Jeżeli byśmy dostali RESTfull API, to prace frontendowe skróciłyby się o te 30% czasu, a pracę backendowe nie wydłużyłyby się, bo po stworzeniu HATEOASa pomału wszyscy dochodzą do wniosku, że lepiej dać projekcje, które mają wszystko co potrzeba.

Dojście do danych

Kolejnym absurdem HATEOASa jest dojście do danych. Załóżmy, że chcemy wyświetlić wszystkie artykuły użytkownika z kategorii fantastyka. Musimy na początek pozyskać jakoś link użytkownika, nie jego id, tylko link do API, potem musimy poprzez odpytanie znaleźć jaki link ma kategoria fantastyka, czyli już mamy dwie odpytania API, a następnie możemy sobie wyrzucić pełne zapytanie, daj mi wszystkie artykuły, gdzie link do użytkownika jest taki, a kategoria ma taki link. No masakra. Zróbmy to inaczej, pobierajmy wszystkie, a co nam tam. Pobierzmy użytkownika, potem pobierzmy jego wszystkie artykuły i pobierzmy wszystkie kategorie, a następnie odfiltrujmy po linku kategorię fantastyka. Znów trzy odpytania. No to projekcja, ale znów zamiast nazwy dostaniemy relacje w postaci linków, czyli jakbyśmy chcieli pokazać, kiedy dana kategoria powstała, to mamy dodatkowe odpytanie.

Z powodu problemów z linkami zamiast odpowiedzi, która ma wszystko, oraz delikatnie mówiąc DDOSowaniem aplikacji, żaden duży dostawca API nie zdecydował się na HATEOASa. Wszyscy pozostają na poziomie 2. Wyobraźcie sobie tylko jakby wyglądała zakładka network w konsoli przeglądarki, gdyby Facebook miał poziom 3?

Szybkość

Kolejnym powodem dlaczego w mojej opinii nie warto, to szybkość. Umówmy się, zapytanie do bazy danych jest szybsze, niż 3 zapytania do bazy danych. Nasunęło mi się nawet takie porównanie, czemu HATEOAS, powinien nazywać się Hate All Of Us.

Mamy dwa urzędy. Jeden ma poziom 2, drugi poziom 3. Dzwonię do tego bardziej dojrzałego.

– Dzień dobry, chciałbym zarejestrować samochód.

– Poproszę o dane oraz o numer urzędowy dowodu rejestracyjnego.

– Nie mam numeru urzędowego dowodu rejestracyjnego tylko normalny.

– To proszę zadzwonić tu xxx.

Dzwonię:

– Dzień dobry mam dowód rejestracyjny o numerze yyy, chciałbym uzyskać numer urzędowy.

– Proszę, to Pana numer zzz.

Dzwonię, do pierwszego, podaje numer:

– Super, udało się. Teraz poproszę pana id w naszej bazie.

– Ale ja nie znam tego id.

– No to proszę zadzwonić xxx.

Dzwonię, podaje PESEL i dostaje to Id, oddzwaniam i modlę się, że to wszystko, czego potrzebuję do rejestracji samochodu.

Poziom mniej dojrzały, poziom 2.

– Dzień dobry, chciałbym zarejestrować samochód.

– Poproszę o dane oraz o numer urzędowy dowodu rejestracyjnego.

– Nie mam numeru urzędowego dowodu rejestracyjnego tylko normalny.

– To poproszę o ten normalny numer.

– Proszę: yyy.

– Teraz potrzebuje Pana id urzędowe.

– Nie znam.

– To poproszę o numer PESEL.

– Proszę: uuu.

– Dobrze, mamy już wszystko.

W drugim przypadku nie tracę czasu na zdobywanie danych z rozsianych linków, wysyłam jedną paczkę danych i dostaje wszystko czego potrzebuję. Od strony backendu HATEOS to wspaniała sprawa, bo projektant API ma mało roboty, większość upierdliwej pracy robi się samo. Natomiast każdy kto musi to API skonsumować, będzie przeżywał gehennę. W mojej opinii ma to zastosowanie w architekturze rozproszonej, kiedy pogniewaliśmy się na architekturę Zakasa-Osmaniego, która to sprawia, że możemy znów zrobić RESTfull API w świecie rozproszonych aplikacji.

Poważnie, nie znajduję żadnej wyższości REST API nad RESTfull API. RESTfull API też może mieć swoją strukturę. Choć jeżeli dobrze stosujemy kody nagłówków http, to możemy po samym kodzie nagłówka zasugerować, co jest w odpowiedzi i czy jest to błąd, czy to co chcemy. Jeśli macie inne zdanie, dajcie znać w komentarzach.


baner

Podobne artykuły

[wpdevart_facebook_comment curent_url="https://geek.justjoin.it/hateoas-hate-all-of-us/" order_type="social" width="100%" count_of_comments="8" ]