Oskar Dudycz

Pragmatic about programming

Czy pisanie generatora klienta API to dobry pomysł?

2021-04-19 oskar dudyczSztuka Programowania

cover

Cześć!

W zeszłym tygodniu napisałem, że nudne, powtarzalne sytuacje trzeba automatyzować. Są jednak takie sytuacje, gdy lepiej tego nie robić. Dosyć regularnie widzę, że ludzie chcą generować kliencki kod TypeScript na podstawie API/Backendu. Gdy pytają mnie “Czy warto?” to często odpowiadam, że zrobiłem to dwa razy: pierwszy i ostatni.

Jest wiele pomysłów, które na początku wydają nam się kuszące. Żałujemy, że zaczęliśmy wprowadzać je w życie. Pisałem o tym przy okazji rozważań na temat generycznych worfklow. Niestety przy podejmowaniu decyzji mamy tendencje do lekceważenia skomplikowania problemu. Gdy już to robiliśmy, to wydaje nam się, że nie popełnimy tym razem tych samych błędów. Gdy pracujemy nad czymś nowym, to zwykle nie mamy pełni wiedzy o konsekwencjach.

Najbardziej nas zabija to skomplikowanie, którego nie przewidzieliśmy (accidental complexity).

Zanim przejdę do problemów z generowaniem kodu klienckiego, omówię zalety generowania klienta na podstawie API.

  1. Jedno źródło prawdy. Zmieniamy w jednym miejscu definicję API, która jest potem propagowana do aplikacji klienckich.
  2. Mniej kopiuj/wklej. Skoro kod będziemy generować automatycznie, to nie będziemy musieli już kopiować i wklejać go we wszystkich miejscach. Możemy zamknąć to sobie w pakiecie (np. NPM) i zaktualizować go gdy API się zmieni.
  3. Pozbywamy się odpowiedzialności. Dotyczy to szczególnie backendowców. Wiele osób uważa, że wystawienie endpointa kończy ich pracę. To, że jeszcze wygenerowali kod kliencki, uważają za bonus, za który powinni dostać medal od frontendowca. Endpoint wystawiony, wdrożony, paczka z kodem TypeScript wypchnięta, robota zrobiona. Reszta już należy do frontendowca.

Skoro jest tak pięknie, to czemu jest tak źle?

  1. Okazuje się, że wcale nie ma jednego źródła prawdy. Inna wersja API jest na środowisku developerskim, inna na testowym. Jeśli dodamy do tego pracę równoczesną to sprawa staje się jeszcze bardziej skomplikowana. Rzadko mamy komfort, że najpierw powstaje endpoint, a potem dopiero rusza praca nad frontendem. Zwykle prace idą równolegle. Gdy mamy część API z feature brancha, a część normalnego to wersjonowanie robi się coraz trudniejsze. Jak numerować paczki prerelease’owe? Czy powinienem używać paczki ‘1.0.0-beta+exp.sha.5114f85’ czy ‘1.0.0-beta+exp.sha.ae27z32’? Jeśli dodamy do tego środowisko mikroserwisowe gdzie każdy moduł może mieć swoje API to głowa zaczyna już boleć.
  2. Ilość konfliktów i poprawiania typów nas przytłacza. Fajnie, że typy mamy wygenerowane i wywołanie API też. Podmiana zachowania API to nie tylko zmiana kontraktu. Za tym wszystkim idą też zmiany w logice aplikacji. Zwykle dostajemy błędy kompilacji po podbiciu paczki i musimy się zastanowić “co kompilator ma na myśli”. Szczególnie jeśli backendowiec uznał, że robota skończona i nie ma dobrej komunikacji. Wtedy zrozumienie co się pozmieniało i dlaczego coś nie działa na podstawie błędu kompilacji to tragedia. Może tutaj nie być wcale złej woli. Jeżeli mamy API generowane na podstawie np. API Gateway to może się okazać, że podbijamy paczkę i trafiamy na niespodziankę. Poza nowym polem, które chcieliśmy dodać, dostajemy w pakiecie masę zmian z innego modułu. Kod się nie kompiluje, nie wiemy jak naprawić, inny zespół nie ma na razie czasu, żeby się tym zająć. Kto pierwszy ten lepszy gorszy. Dotknąłeś? To teraz sobie poprawiaj.
  3. Możemy zrobić rozproszony monolit. Częstą praktyką nawet w architekturze mikroserwisowej jest stawianie ujednoliconego API Gateway. Dzięki temu aplikacje klienckie używają ujednoliconego interfejsu (ten sam URL itd.) dla wszystkich API. Pod spodem wywołanie może być odesłane do konkretnego serwisu. Często też, mimo że mamy architekturę mikroserwisową na backend to mamy jedną monolityczną aplikację webową. Jeśli dla takiego API wygenerujemy ujednoliconą paczkę kliencką to nie dość, że będziemy mieli problemy opisane powyżej, to jeszcze możemy sobie zabetonować continuous delivery. Nagle okazuje się, że ktoś wpadnie na pomysł “wprowadźmy harmonogram wdrożeń!” albo “wprowadźmy code freeze!”. Gdy jesteśmy już przygnieceni różnymi błędami. Gdy nie jesteśmy w stanie opanować konfliktów po wdrożeniu API, to mogą szalone pomysły przyjść do głowy. Wtedy, mimo że backend “w teorii” jest gotowy na ciągłe wdrażanie zmian, to zaczynamy podejmować paniczne decyzje. Mrozimy wdrożenia, ustalamy kolejność wydawniczą itd. Oczywiście rozwiązujemy tym samym problem, a nie źródło problemu.
  4. Dodatkowo wcale nie mamy pewności, że używamy właściwego kontraktu. To, że wygenerowaliśmy nową paczkę, wcale nie znaczy, że ona jest już użyta na frontendzie. Nawet jeśli jest użyta, to nie znaczy, że jest wdrożona gdziekolwiek. Nigdy nie będzie sytuacji, że API będzie ujednolicone z klientem od razu. Nawet jeśli zrobimy über automatyzację, która sama podbije paczki, wmerdżuje, wdroży to i tak będzie to z opóżnieniem. Dopóki nie podbijemy paczki, nigdy nie wiemy, czy mamy aktualny kod. Co więcej, nawet jak ją podbijemy, to nie mamy pewności. W międzyczasie mogła już wyjść kolejna. Mógł być też błąd w generowaniu i się nie wydała.
  5. Pisanie własnego generatora to walka z wiatrakami. Nigdy nie będzie on priorytetem w utrzymaniu. Użycie czyjegoś generatora to z kolei walka z czyimiś pomysłami i niedopasowaniami do naszego problemu.

Ręczne zmienianie kontraktów nie zlikwiduje problemu. Pozostawia jednak, w nas przeświadczenie, że to jest coś co może nie być aktualne. Gdy używamy wygenerowanego kodu to mamy tendencje do myślenia podświadomie, że kontrakt jest aktualny.

Kiedy ma to sens?

  1. Gdy nie robimy Breaking Change. Pisałem o tym ze szczegółami w mailu “Dbajmy o siebie! Czyli rzecz o kompatybilności”. Podstawowa zasada, która powinna nam przyświecać to “Primum non nocere”. Kiedy można robić breaking change? Najlepiej nigdy. Jeśli mamy stabilne API, albo łączymy się z API zewnętrznym, to może mieć to sens.
  2. Gdy rozmawiamy ze sobą. Nic nie zastąpi dobrej definicji kontraktu. Żadne generowanie kodu nie zastąpi braku designu i gdy zespoły ze sobą nie współpracują. Jako programiści musimy pamiętać, że zrobienie funkcjonalności kończy się, gdy klient zaczyna ją używać. Nie kończy się, gdy wmerdżujemy kod API albo formatki. Kontrakt najlepiej ustalać przed programowaniem, wtedy wiele rzeczy się ułatwia. Zerknij na mój wpis “Sociological aspects of Microservices”.

Pozdrawiam!

Oskar

P.S. W zeszłym tygodniu napisałem na blogu o tym, czy na pewno zdarzenia powinny być jak najmniejsze: https://event-driven.io/en/events_should_be_as_small_as_possible/. Jest tam też o rozproszonym monolicie. Zachęcam do lektury!

A oprócz tego, jak co tydzień nowe Architecture Weekly: https://github.com/oskardudycz/ArchitectureWeekly/discussions/19

  • © Oskar Dudycz 2019-2020