Interfejsy API kontroli wersji

9

Załóżmy, że masz duży projekt obsługiwany przez bazę API. Projekt udostępnia także publiczny interfejs API, z którego mogą korzystać użytkownicy końcowi (ish).

Czasami musisz wprowadzić zmiany w bazie API obsługującej Twój projekt. Na przykład musisz dodać funkcję, która wymaga zmiany interfejsu API, nowej metody lub wymaga zmiany jednego z obiektów lub formatu jednego z tych obiektów, przekazanych do lub z interfejsu API.

Zakładając, że używasz również tych obiektów w publicznym interfejsie API, obiekty publiczne zmienią się również za każdym razem, gdy to zrobisz, co jest niepożądane, ponieważ Twoi klienci mogą polegać na obiektach API, które pozostają identyczne, aby działał ich kod analizujący. (kaszel klientów WSDL C ++ ...)

Tak więc jednym potencjalnym rozwiązaniem jest wersja API. Ale kiedy mówimy „wersja” interfejsu API, brzmi to tak, że musi to również oznaczać wersję obiektów API, a także zapewnienie duplikatów wywołań metod dla każdej zmienionej sygnatury metody. Więc miałbym wtedy zwykły stary obiekt clr dla każdej wersji mojego interfejsu API, co znów wydaje się niepożądane. I nawet jeśli to zrobię, na pewno nie będę budował każdego obiektu od zera, ponieważ skończyłoby się to ogromną ilością powielonego kodu. Raczej API może rozszerzyć prywatne obiekty, których używamy do naszego podstawowego API, ale potem napotykamy ten sam problem, ponieważ dodane właściwości byłyby również dostępne w publicznym API, gdy nie powinny.

Jaki jest zatem rozsądek, który zwykle stosuje się w tej sytuacji? Wiem, że wiele usług publicznych, takich jak Git dla Windows, utrzymuje wersjonowany interfejs API, ale mam problem z wyobrażeniem sobie architektury, która to obsługuje bez ogromnej ilości zduplikowanego kodu obejmującego różne metody wersjonowane i obiekty wejścia / wyjścia.

Zdaję sobie sprawę, że procesy takie jak semantyczna wersja wersjonowania próbują wprowadzić rozsądek, kiedy powinny wystąpić przerwy w publicznym API. Problem polega na tym, że wydaje się, że wiele lub większość zmian wymaga zepsucia publicznego interfejsu API, jeśli obiekty nie są bardziej oddzielone, ale nie widzę dobrego sposobu na zrobienie tego bez powielania kodu.

Walizka
źródło
1
I don't see a good way to do that without duplicating code- Twój nowy interfejs API zawsze może wywoływać metody w starym interfejsie API lub odwrotnie.
Robert Harvey,
2
AutoMapper na ratunek, niestety tak - potrzebujesz odrębnych wersji każdej umowy, nie zapomnij, że wszystkie obiekty, do których odwołuje się twoja umowa, są częścią tej umowy. W rezultacie Twoja rzeczywista implementacja powinna mieć własną pojedynczą wersję modeli i musisz przekształcić jedną wersję w różne wersje umowy. AutoMapper może ci w tym pomóc, a także uczynić modele wewnętrzne mądrzejszymi niż modele kontraktowe. Wobec braku AutoMappera użyłem metod rozszerzeń do stworzenia prostych tłumaczeń między modelami wewnętrznymi i modelami kontraktowymi.
Jimmy Hoffa,
Czy istnieje do tego konkretna platforma / kontekst? (tj. biblioteki DLL, interfejs API REST itp.)
GrandmasterB
.NET z klasami interfejsu użytkownika MVC i Webforms, dll. Mamy zarówno API odpoczynku, jak i mydła.
Sprawa

Odpowiedzi:

6

Podczas utrzymywania interfejsu API, który jest używany przez strony trzecie, nieuniknione jest, że będziesz musiał wprowadzić zmiany. Poziom złożoności będzie zależeć od rodzaju zachodzących zmian. Oto główne pojawiające się scenariusze:

  1. Dodano nową funkcjonalność do istniejącego interfejsu API
  2. Stara funkcjonalność wycofana z interfejsu API
  3. Istniejąca funkcjonalność w API zmienia się w jakiś sposób

Nowa funkcjonalność dodająca do istniejącego API

To najłatwiejszy scenariusz do wsparcia. Dodanie nowych metod do interfejsu API nie powinno wymagać żadnych zmian w istniejących klientach. Można to bezpiecznie wdrożyć dla klientów, którzy potrzebują nowej funkcjonalności, ponieważ nie ma żadnych aktualizacji dla żadnego istniejącego klienta.

Stara funkcjonalność wycofana z interfejsu API

W tym scenariuszu musisz poinformować istniejących użytkowników interfejsu API, że ta funkcjonalność nie będzie obsługiwana w dłuższej perspektywie. Dopóki nie zrezygnujesz z obsługi starej funkcjonalności (lub dopóki wszyscy klienci nie zostaną zaktualizowani do nowej funkcjonalności), musisz zachować starą i nową funkcjonalność API w tym samym czasie. Jeśli jest to biblioteka pod warunkiem, że większość języków ma sposób na oznaczenie starych metod jako przestarzałe / przestarzałe. Jeśli jest to jakaś usługa strony trzeciej, zwykle najlepiej jest mieć różne punkty końcowe dla starej / nowej funkcjonalności.

Istniejąca funkcjonalność w API zmienia się w jakiś sposób

Ten scenariusz będzie zależeć od rodzaju zmiany. Jeśli parametry wejściowe nie muszą być już używane, możesz po prostu zaktualizować usługę / bibliotekę, aby zignorować teraz dodatkowe dane. W bibliotece byłoby przeciążone wewnętrznie wywołanie metody nowej metody, która wymaga mniej parametrów. W usłudze hostowanej punkt końcowy ignoruje dodatkowe dane i może obsługiwać oba typy klientów i uruchamiać tę samą logikę.

Jeśli istniejąca funkcjonalność wymaga dodania nowych wymaganych elementów, musisz mieć dwa punkty końcowe / metody dla swojej usługi / biblioteki. Do czasu aktualizacji klientów musisz obsługiwać obie wersje.

inne przemyślenia

Rather, the API is likely to extend the private objects we are using for our base API, but then we run into the same problem because added properties would also be available in the public API when they are not supposed to be.

Nie narażaj wewnętrznych prywatnych obiektów za pośrednictwem biblioteki / usługi. Twórz własne typy i mapuj wewnętrzną implementację. Umożliwi to dokonanie zmian wewnętrznych i zminimalizowanie liczby aktualizacji wymaganych przez klientów zewnętrznych.

The problem is more that it seems like many or most changes require breaking the public API if the objects aren't more separated, but I don't see a good way to do that without duplicating code.

Interfejs API, niezależnie od tego, czy jest usługą, czy biblioteką, musi być stabilny w punkcie integracji z klientami. Im więcej czasu poświęcisz na określenie, jakie powinny być dane wejściowe i wyjściowe, i zachowaj je, ponieważ oddzielne jednostki pozwolą ci zaoszczędzić wielu problemów na drodze. Ustaw interfejs API jako osobny byt i zamapuj na klasy, które zapewniają prawdziwą pracę. Czas zaoszczędzony na zmianie wewnętrznych implementacji powinien bardziej zrównoważyć dodatkowy czas potrzebny na zdefiniowanie dodatkowego interfejsu.

Nie traktuj tego kroku jako „powielającego się kodu”. Choć są podobne, są to oddzielne byty, które warto poświęcić chwilę. Podczas gdy zmiany zewnętrznego interfejsu API prawie zawsze wymagają odpowiedniej zmiany wewnętrznej implementacji, zmiany wewnętrznej implementacji nie zawsze powinny zmieniać zewnętrzny interfejs API.

Przykład

Załóżmy, że udostępniasz rozwiązanie do przetwarzania płatności. Korzystasz z PaymentProviderA do przeprowadzania transakcji kartą kredytową. Później uzyskasz lepszą stawkę dzięki procesorowi płatności PaymentProviderB. Jeśli w interfejsie API widoczne są pola Adres karty kredytowej / rachunku zamiast typu PaymentProviderA, zmiana interfejsu API wynosi 0, ponieważ interfejs pozostał taki sam (mam nadzieję, że mimo to, jeśli PaymentProviderB wymaga danych, które nie były wymagane przez PaymentProviderA, należy wybrać jedną z opcji: wspierać oba lub utrzymać gorszą stawkę z PaymentProviderA).

Phil Patterson
źródło
Dziękuję za tę bardzo szczegółową odpowiedź. Czy znasz jakieś przykłady projektów o otwartym kodzie źródłowym, które mógłbym przejrzeć, aby dowiedzieć się, jak to zrobiły istniejące projekty? Chciałbym zobaczyć kilka konkretnych przykładów tego, jak zorganizowali kod, który pozwala im to zrobić bez kopiowania ich różnych kodów konstrukcyjnych POCO do różnych obiektów wersji, ponieważ jeśli wywołasz metody współdzielone, będziesz musiał go podzielić metody współdzielonej, aby móc edytować ten obiekt dla obiektu wersjonowanego.
Sprawa
1
Nie znam żadnych dobrych przykładów z głowy. Jeśli będę miał czas w ten weekend, mógłbym stworzyć aplikację testową, aby rzucić na GitHub, aby pokazać, jak niektóre z tych rzeczy można zaimplementować.
Phil Patterson
1
Problem polega na tym, że z wysokiego poziomu istnieje tak wiele różnych sposobów na zminimalizowanie sprzężenia między klientami a serwerami. WCF ma interfejs o nazwie IExtensibleDataObject, który pozwala przekazywać dane, które nie są zawarte w umowie, od klienta i przesyłać je przewodowo do serwera. Google stworzył Protobuf do komunikacji między systemami (istnieją implementacje open source dla .NET, Java itp.). Ponadto istnieje wiele systemów opartych na komunikatach, które mogą również działać (przy założeniu, że proces można wykonać asynchronicznie).
Phil Patterson
Może nie być złym pomysłem, aby pokazać konkretny przykład duplikacji kodu, który próbujesz usunąć (jako pytanie o przepełnienie nowego stosu) i zobaczyć, jakie rozwiązania wymyśli społeczność. Trudno jest udzielić ogólnej odpowiedzi na pytanie; więc konkretny scenariusz może być lepszy.
Phil Patterson