Jak zarządzasz podstawową bazą kodu dla wersjonowanego interfejsu API?

105

Czytałem o strategiach wersjonowania dla interfejsów API ReST i coś, czego żaden z nich nie wydaje się uwzględniać, dotyczy sposobu zarządzania podstawową bazą kodu.

Załóżmy, że wprowadzamy kilka istotnych zmian w interfejsie API - na przykład zmieniamy nasz zasób klienta, aby zwracał oddzielne pola forenamei surnamepola zamiast jednego namepola. (W tym przykładzie użyję rozwiązania do wersjonowania adresów URL, ponieważ łatwo jest zrozumieć związane z tym pojęcia, ale pytanie dotyczy również negocjacji treści lub niestandardowych nagłówków HTTP)

Mamy teraz punkt końcowy w http://api.mycompany.com/v1/customers/{id}i inny niezgodny punkt końcowy w http://api.mycompany.com/v2/customers/{id}. Wciąż publikujemy poprawki błędów i aktualizacje zabezpieczeń API v1, ale rozwój nowych funkcji skupia się teraz na wersji v2. Jak piszemy, testujemy i wdrażamy zmiany na naszym serwerze API? Widzę co najmniej dwa rozwiązania:

  • Użyj gałęzi / tagu kontroli źródła dla bazy kodu v1. v1 i v2 są opracowywane i wdrażane niezależnie, z połączeniami kontroli wersji używanymi w razie potrzeby do zastosowania tej samej poprawki błędu do obu wersji - podobnie jak w przypadku zarządzania bazami kodów dla aplikacji natywnych podczas opracowywania nowej głównej wersji, przy jednoczesnym wsparciu poprzedniej wersji.

  • Spraw, aby baza kodu była świadoma wersji interfejsu API, dzięki czemu otrzymujesz jedną bazę kodu, która obejmuje zarówno reprezentację klienta w wersji 1, jak i reprezentację klienta w wersji 2. Traktuj przechowywanie wersji jako część architektury swojego rozwiązania, a nie jako problem z wdrożeniem - prawdopodobnie używając kombinacji przestrzeni nazw i routingu, aby upewnić się, że żądania są obsługiwane przez właściwą wersję.

Oczywistą zaletą modelu gałęzi jest to, że usunięcie starych wersji API jest trywialne - po prostu przestań wdrażać odpowiednią gałąź / tag - ale jeśli używasz kilku wersji, możesz skończyć z naprawdę zawiłą strukturą gałęzi i potokiem wdrażania. Model „ujednoliconej bazy kodu” pozwala uniknąć tego problemu, ale (jak sądzę?) Znacznie utrudniłby usunięcie przestarzałych zasobów i punktów końcowych z bazy kodu, gdy nie są już potrzebne. Wiem, że jest to prawdopodobnie subiektywne, ponieważ jest mało prawdopodobne, aby była prosta, poprawna odpowiedź, ale jestem ciekawy, jak organizacje, które utrzymują złożone interfejsy API w wielu wersjach, rozwiązują ten problem.

Dylan Beattie
źródło
41
Dzięki za zadanie tego pytania! Nie mogę uwierzyć, że więcej ludzi nie odpowiada na to pytanie !! Mam dość tego, że wszyscy mają opinię na temat tego, w jaki sposób wersje wchodzą do systemu, ale nikt nie wydaje się rozwiązywać prawdziwego trudnego problemu wysyłania wersji do odpowiedniego kodu. Do tej pory powinien istnieć przynajmniej szereg akceptowanych „wzorców” lub „rozwiązań” tego pozornie powszechnego problemu. Jest niesamowita liczba pytań dotyczących SO dotyczących "wersjonowania API". Decyzja o akceptacji wersji jest PROSTA FRIKKIN (względnie)! Obsługa go w bazie kodu, gdy już się pojawi, jest TRUDNA!
arijeet

Odpowiedzi:

45

Użyłem obu wspomnianych strategii. Spośród tych dwóch preferuję drugie podejście, które jest prostsze w przypadkach użycia, które je obsługują. Oznacza to, że jeśli potrzeby dotyczące wersjonowania są proste, wybierz prostszy projekt oprogramowania:

  • Mała liczba zmian, zmiany o małej złożoności lub harmonogram zmian z małą częstotliwością
  • Zmiany, które są w dużej mierze ortogonalne w stosunku do reszty kodu: publiczny interfejs API może spokojnie istnieć z resztą stosu bez konieczności „nadmiernego” (niezależnie od definicji tego terminu) rozgałęzienia w kodzie

Nie było dla mnie zbyt trudne usunięcie przestarzałych wersji przy użyciu tego modelu:

  • Dobre pokrycie testowe oznaczało, że wyrywanie wycofanego interfejsu API i związanego z nim kodu zapasowego nie zapewniało (cóż, minimalnych) regresji
  • Dobra strategia nazewnictwa (nazwy pakietów z wersjami API lub nieco brzydsze wersje API w nazwach metod) ułatwiła zlokalizowanie odpowiedniego kodu
  • Kwestie przekrojowe są trudniejsze; modyfikacje podstawowych systemów zaplecza w celu obsługi wielu interfejsów API muszą być bardzo ostrożnie rozważone. W pewnym momencie koszt obsługi wersji (patrz komentarz dotyczący „nadmiernego” powyżej) przeważa nad korzyściami wynikającymi z pojedynczej bazy kodu.

Pierwsze podejście jest z pewnością prostsze z punktu widzenia ograniczenia konfliktu między współistniejącymi wersjami, ale narzut związany z utrzymaniem oddzielnych systemów przeważał nad korzyściami wynikającymi z ograniczenia konfliktu wersji. To powiedziawszy, stworzenie nowego publicznego stosu API i rozpoczęcie iteracji w oddzielnej gałęzi API było bardzo proste. Oczywiście strata pokoleniowa nadeszła niemal natychmiast, a gałęzie zamieniły się w bałagan fuzji, scalania rozwiązań konfliktów i innych podobnych zabaw.

Trzecie podejście dotyczy warstwy architektonicznej: zastosuj wariant wzorca Facade i wyodrębnij swoje interfejsy API do publicznych, wersjonowanych warstw, które komunikują się z odpowiednią instancją Facade, która z kolei komunikuje się z zapleczem za pośrednictwem własnego zestawu interfejsów API. Twoja fasada (użyłem adaptera w moim poprzednim projekcie) staje się własnym pakietem, samowystarczalnym i testowalnym, i pozwala na migrację interfejsów API frontendu niezależnie od zaplecza i od siebie nawzajem.

To zadziała, jeśli twoje wersje API mają tendencję do ujawniania tego samego rodzaju zasobów, ale z różnymi reprezentacjami strukturalnymi, jak w przykładzie imię i nazwisko / nazwisko. Staje się nieco trudniejsze, jeśli zaczną polegać na różnych obliczeniach zaplecza, na przykład „Moja usługa backendu zwróciła nieprawidłowo obliczony procent składany, który został ujawniony w publicznym interfejsie API v1. Nasi klienci już załatali to nieprawidłowe zachowanie. Dlatego nie mogę tego zaktualizować obliczenia w zapleczu i niech będzie obowiązywać do wersji 2. Dlatego teraz musimy rozwidlić nasz kod obliczania odsetek. " Na szczęście zdarza się to rzadko: praktycznie rzecz biorąc, konsumenci RESTful API preferują dokładne reprezentacje zasobów zamiast kompatybilności wstecznej typu bug-for-bug, nawet wśród nierozerwalnych zmian w teoretycznie idempotentnym GETtedzie.

Będę zainteresowany Twoją ostateczną decyzją.

Palpatim
źródło
5
Ciekawe, czy w kodzie źródłowym kopiujesz modele między v0 i v1, które się nie zmieniły? A może masz v1, używając niektórych modeli v0? Dla mnie byłbym zdezorientowany, gdybym zobaczył v1 używający modeli v0 dla niektórych pól. Ale z drugiej strony zmniejszyłoby to nadmiar kodu. Czy do obsługi wielu wersji musimy po prostu zaakceptować powielający się kod dla modeli, które nigdy się nie zmieniły i żyć z nim?
EdgeCaseBerg
1
Pamiętam, że nasze modele z wersjami kodu źródłowego są niezależne od samego interfejsu API, więc na przykład API v1 może używać Modelu V1, a API v2 może również używać Modelu V1. Zasadniczo wewnętrzny wykres zależności dla publicznego interfejsu API obejmował zarówno udostępniony kod API, jak i kod „realizacji” zaplecza, taki jak kod serwera i modelu. W przypadku wielu wersji jedyną strategią, jaką kiedykolwiek stosowałem, jest powielanie całego stosu - podejście hybrydowe (moduł A jest zduplikowany, moduł B jest wersjonowany ...) wydaje się bardzo zagmatwany. YMMV oczywiście. :)
Palpatim
2
Nie jestem pewien, czy postępuję zgodnie z sugestiami dotyczącymi trzeciego podejścia. Czy są jakieś publiczne przykłady kodu o podobnej strukturze?
Ehtesh Choudhury
13

Dla mnie drugie podejście jest lepsze. Użyłem go do usług sieciowych SOAP i planuję używać go również do REST.

Podczas pisania kod powinien uwzględniać wersję, ale warstwa zgodności może być używana jako oddzielna warstwa. W Twoim przykładzie baza kodu może generować reprezentację zasobów (JSON lub XML) z imieniem i nazwiskiem, ale warstwa zgodności zmieni ją tak, aby miała tylko nazwę.

Baza kodu powinna implementować tylko najnowszą wersję, powiedzmy v3. Warstwa kompatybilności powinna konwertować żądania i odpowiedzi między najnowszą wersją v3 a obsługiwanymi wersjami, np. V1 i v2. Warstwa zgodności może mieć oddzielne adaptery dla każdej obsługiwanej wersji, które można połączyć w łańcuch.

Na przykład:

Żądanie klienta v1: v1 adapt to v2 ---> v2 adapt to v3 ----> codebase

Żądanie klienta v2: v1 adapt to v2 (skip) ---> v2 adapt to v3 ----> codebase

Aby uzyskać odpowiedź, adaptery działają po prostu w przeciwnym kierunku. Jeśli korzystasz z języka Java EE, możesz na przykład łańcuch filtrów serwletów jako łańcuch adapterów.

Usunięcie jednej wersji jest łatwe, usuń odpowiedni adapter i kod testowy.

S.Stavreva
źródło
Trudno jest zagwarantować zgodność, jeśli zmieniła się cała podstawowa baza kodu. O wiele bezpieczniej jest zachować stary kod w wydaniach poprawiających błędy.
Marcelo Cantos
5

Rozgałęzianie wydaje mi się znacznie lepsze i zastosowałem to podejście w moim przypadku.

Tak, jak już wspomniałeś - backportowanie poprawek błędów będzie wymagało pewnego wysiłku, ale jednocześnie obsługa wielu wersji w ramach jednej bazy źródłowej (z routingiem i wszystkimi innymi rzeczami) będzie wymagać, jeśli nie mniej, ale przynajmniej takiego samego wysiłku, dzięki czemu system będzie bardziej skomplikowane i potworne z różnymi gałęziami logiki wewnątrz (w pewnym momencie wersjonowania z pewnością dojdziesz do ogromnego case()wskazania na moduły wersji, które mają zduplikowany kod lub mają gorzej if(version == 2) then...). Nie zapominaj również, że do celów regresji nadal musisz mieć rozgałęzione testy.

Jeśli chodzi o politykę wersjonowania: chciałbym zachować maksymalnie -2 wersje od obecnych, wycofując wsparcie dla starych - to dałoby użytkownikom motywację do zmiany.

edmarisov
źródło
W tej chwili myślę o testowaniu w jednej bazie kodów. Wspomniałeś, że testy zawsze będą musiały być rozgałęzione, ale myślę, że wszystkie testy dla wersji v1, v2, v3 itd. Również mogłyby funkcjonować w tym samym rozwiązaniu i wszystkie być uruchamiane w tym samym czasie. Myślę o dekorowanie testy z atrybutów, które określają co wersje obsługują one: np [Version(From="v1", To="v2")], [Version(From="v2", To="v3")], [Version(From="v1")] // All versions po prostu zwiedzania to teraz, słyszał ktoś to zrobić?
Lee Gunn
1
Otóż ​​po 3 latach przekonałem się, że nie ma dokładnej odpowiedzi na pierwotne pytanie: D. Jest to bardzo zależne od projektu. Jeśli możesz sobie pozwolić na zamrożenie API i tylko utrzymanie go (np. Poprawki błędów), to nadal rozgałęziłbym / odłączył powiązany kod (logika biznesowa związana z API + testy + reszta punktu końcowego) i miałbym wszystkie wspólne rzeczy w osobnej bibliotece (z własnymi testami ). Jeśli V1 ma współistnieć z V2 przez dłuższy czas, a prace nad funkcjami nadal trwają, trzymałbym je razem i testy również (obejmujące V1, V2 itp. I odpowiednio nazwane).
edmarisov
1
Dzięki. Tak, wydaje się, że to dość uparta przestrzeń. Najpierw spróbuję jednego rozwiązania i zobaczę, jak to działa.
Lee Gunn
0

Zazwyczaj wprowadzenie większej wersji API prowadzącej do sytuacji konieczności utrzymywania wielu wersji jest zdarzeniem, które nie występuje (lub nie powinno) występować bardzo często. Jednak nie da się tego całkowicie uniknąć. Myślę, że ogólnie można bezpiecznie założyć, że po wprowadzeniu wersja główna pozostanie najnowszą wersją przez stosunkowo długi czas. Na tej podstawie wolałbym osiągnąć prostotę kodu kosztem powielania, ponieważ daje mi to większą pewność, że nie będę łamał poprzedniej wersji, kiedy wprowadzam zmiany w najnowszej.

user1537847
źródło