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 forename
i surname
pola zamiast jednego name
pola. (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.
źródło
Odpowiedzi:
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:
Nie było dla mnie zbyt trudne usunięcie przestarzałych wersji przy użyciu tego modelu:
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
GET
tedzie.Będę zainteresowany Twoją ostateczną decyzją.
źródło
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.
źródło
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ą gorzejif(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.
źródło
[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ć?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.
źródło