Zaczynając pracę nad Star Trek API postanowiłem, że będzie można się z nim komunikować zarówno przez protokół SOAP, jak i za pomocą REST-u. O ile w przypadku SOAP-u wybór implementacji w Javie było oczywisty – wsdl2java opakowany w gradle’owy plugin, o tyle w przypadku REST-u nie było to już tak jednoznaczne. Nie ma zresztą jednego standardu do opisu REST-owych encji i serwisów. Ostatecznie zdecydowałem się na najpopularniejszą implementację, jaka istnieje: Swaggera, ale trzeba tutaj oddać, że RAML wydaje się równie dojrzały, co Swagger.
W tym poście opiszę plusy i minusy mojego podejścia, oraz różnice między SOAP-em i REST-em z użyciem Swaggera.
Oczywistym i od razu rzucającym się w oczy minusem tworzenia dwóch wersji modelu API jest konieczność utrzymywania dwóch wersji kodu opisującego encje i serwisy. Jeden kod w XML-u dla SOAP-u, a drugi kod w YAML-u dla REST-u. Ma to jednak plusy, ponieważ wymusza oddzielenie modelu bazodanowego od modelu API warstwą pośrednią, która musi zapewniać REST-owym i SOAP-owym endpointom jednolity model komunikacji i zapobiegnie przeciekaniu wygenerowanego kodu do warstwy repozytoriów. U mnie tę warstwę pośrednią stanowi kilka serwisów plus DTO reprezentujący kryteria wyszukiwania. Oddzielnymi mapperami, zarówno SOAP-owymi, jak i REST-owymi, zapytania są tłumaczone na wspomniany wspólny DTO, który służy do odpytania generycznego repozytorium. Następnie tymi samymi mapperami model bazodanowy jest tłumaczony na encje wygenerowane z XSD-ków i YAML-ów. Wymyśliłem to na samym początku i do tej pory rozwiązanie takie jest wystarczające. Jestem teraz w trakcie przeprowadzania małej rewolucji w modelu danych API (rozszerzanie danych zwracanych z endpointów, podział na metody do wyszukiwania encji i do pobierania pełnej encji, wraz z satelitami) i wymaga to tylko minimalnych zmian w warstwie repozytoriów.
Plusem tworzenia zarówno SOAP-owych kontraktów, jak i REST-owych specyfikacji jest możliwość dotarcia do większego grona potencjalnych odbiorców Star Trek API. Zarówno SOAP, jak i Swagger pozwalają programistom piszącym w wielu różnych językach na stosunkowo tanie wygenerowanie klientów do API. Jakość takiego generowanego kodu może być różna, niemniej stanowi jakiś punkt wyjścia. Ponadto, jeśli klient do mojego API nie będzie działać, to issue na GitHubie powstanie w issue trackerze generatora kodu, a nie w moim projekcie.
A teraz trochę o różnicach.
SOAP pozwala dokładnie opisać zarówno obiekty zapytania, jak i obiekty odpowiedzi, którymi komunikujemy się ze światem zewnętrznym. IDEA, edytor którego używam na co dzień, posiada wsparcie dla WSDL-i i XSD-ków, więc wiele błędów wyłapuje bez konieczności uruchamiania buildu, chociaż, mówiąc uczciwie, IDEA posiada po prostu wsparcie dla walidacji na podstawie XSD, a namespace WSDL-a ma zalinkowanego własnego XSD-ka. Tym niemniej, SOAP to dojrzała technologia, posiadająca stabilny standard opisu obiektów i serwisów. Dodatkowo wsdl2java generuje interfejsy, które implementujemy, aby wystawić na zewnątrz endpointy. Większość popularnych błędów, które popełniałem przy pisaniu WSDL-i i XSD-ków objawiała się sensownymi komunikatami, po których było wiadomo, co powinno zostać poprawione.
Z drugiej strony jest Swagger. Można w nim napisać encje, a z tych encji wygenerować kod javowy. Ta funkcjonalność działa. Można również wygenerować interfejsy, a nawet implementacje dla kodu serwerowego, ale po pobieżnym przejrzeniu kodu, który zostaje wygenerowany – a do wyboru jest tu kilka implementacji – postanowiłem wygenerować encje i klienty, ale napisać kod serwerowy samodzielnie, ponieważ generowały się głupoty.
W Swaggerze każdemu adresowi w API odpowiada metoda w klasie klienta. Zaskoczeniem był dla mnie sygnatury generowanych metod. Wyobraźmy sobie bowiem endpoint do wyszukiwania, do którego można przekazać dwadzieścia albo trzydzieści kryteriów. Co robi wówczas Swagger? Generuje metodę z 30 parametrami! Powodzenia w refaktorowaniu takich metod, gdy pierwsza wersja klienta pójdzie już w świat – wówczas zamiana miejscami kilku argumentów tego samego typu będzie skutkować trudnymi do wychwycenia i naprawienia błędami. Podobnie będzie z dodaniem jakiegoś argumentu w środku już istniejącej listy. Dojście do tego, co właściwie się zmieniło i jak teraz powinno się używać klienta ma potencjał dostarczyć wielu godzin rozrywki. Spodziewałbym się, że klienty swaggerowe będą tworzyły osobne obiekty, które reprezentują parametry wejściowe endpointu, ale tak nie jest. Mam w planach przykrycie swaggerowego klienta czymś właśnie takim, by inni ludzie nie musieli zgadywać, co się zmieniło w moim API.
Swagger w żaden sposób nie opracował wsparcia dla zagnieżdżonych obiektów w zapytaniach. Ostatecznie, po kilku próbach i błędach doszedłem do wniosku, że do transportu listy złożonych obiektów w zapytaniu najlepiej będzie mi na piechotę napisać serializator i deserializator. Być może przed ustabilizowaniem wersji beta wprowadzę w miejsce złożonych obiektów zwykłego JSON-a.
Swaggerowe specyfikacje można napisać zarówno w YAML-u, jak i w JSON-ie, mamy więc wybór między dwoma wymiennymi formatami. Gdy uczyłem się pisać specyfikacje, było to dla mnie tylko utrudnieniem. Tutoriale, opisy błędów i StackOverflow pełne są przykładów działającego i niedziałającego kodu zarówno w YAML-u, jak i w JSON-ie. Sam zdecydowałem się na YAML i za każdym razem, gdy widziałem kod w JSON-nie, musiałem go sobie tłumaczyć w głowie na YAML, co przychodziło mi z mozołem. Rozumiem możliwość zapisania czegoś w więcej niż jednym formacie w przypadku Spring Batcha i jego definicji XML-owych i javowych, bo tam cele i ograniczenia są nieco inne. Rozumiem też to podejście w przypadku ORM-ów, które mogą korzystać z adnotacji na encjach albo z leżących obok tych encji XML-i (jak w przypadki Doctrine w PHP, gdzie na środowisku może po prostu być wyłączony Tokenizer, a więc w rezultacie adnotacje). Ale tutaj rozumiem mniej. W Swaggerze mamy dwa szeroko rozpowszechnione, równie czytelne dla ludzi i serializujące się tak samo formaty danych. Ale może to wybór poczyniony z tego samego powodu, dla którego ja piszę na raz API REST-owe i SOAP-owe – żeby więcej ludzi skorzystało z rozwiązania.
Kolejnym minusem Swaggera jest wyjątkowo rozluźnione traktowanie błędów. Przykładowo, gdy specyfikując typ pola w encji wpisałem strin zamiast string, pole po prostu się nie wygenerowało, ale nie było żadnego komunikatu o błędzie, a na pewno nie taki, za którym idzie przerwanie buildu. Podobnie w przypadku, gdy pomyli się nazwy encji linkowanych w specyfikacji endpointów. Swagger po prostu wygeneruje kod javowy, który się nie skompiluje, bo klient API będzie wskazywał na nieistniejącą klasę.
Rozpisałem się o minusach Swaggera, ale to nie jest tak, że nie da się go używać. Da się. Używam. Ale przy następnej okazji spróbuję RAML-a.
Zaczynałem ten eksperyment bez żadnego biasu. Jedyną rzeczą, którą od początku się spodziewałem, było to, że barokowe XML-e będą zajmowały więcej miejsca, niż YAML-owe specyfikacje. Ale i to nie do końca się sprawdziło. Jeśli chodzi o miejsce zajmowane przez YAML-e w pionie, jest go nawet kilka procent więcej. Jeśli chodzi o miejsce zajmowane w poziomie, jest go oczywiście mniej.
Nigdy nie miałem okazji budować API, które udostępnia te same operacje za pomocą REST-u i SOAP-u. Po napisaniu kilkunastu takich samych endpointów REST-owych i SOAP-owych nabrałem przekonania, że REST z użyciem Swaggera nie ma żadnych przewag nad SOAP-em. Natomiast przewagi SOAP-u na Swaggerem to, między innymi, możliwość wysyłania złożonych obiektów w zapytaniach, sensowniejszy generowany kod, lepsza walidacja, lepsze wsparcie w IDE i generalnie dojrzalszy i lepiej ustabilizowany ekosystem. Ale Swagger jest jak najbardziej używalny i na pewno się do niego nie zniechęcam, natomiast mam już teraz świadomość jego ograniczeń.