Adapter na poziomie systemu - okładka

Adapter na poziomie systemu – studium przypadku

Opublikowano Kategorie Backend, Czysty kod, DDDCzas czytania 9min

Adapter jest jednym ze wzorców, który można stosować nie tylko na poziomie kodu, ale też systemu. W tym artykule przedstawię przykład systemu, gdzie wykorzystanie Adaptera rozwiązuje kilka problemów. Opowiem również nieco o wadach i zaletach takiego rozwiązania.

Przedstawienie problemu

Przedmiotem problemu jest system, którego główne funkcjonalności zamknięte są w jednym serwisie. Na potrzeby artykułu będę nazywał go Business Service. W ramach serwisu zawarta jest cała logika, istotna z punktu widzenia prowadzonego biznesu.

Business Service integruje się z zewnętrzną aplikacją. Na potrzeby artykułu nazwę ją External Application. Z punktu widzenia omawianego problemu nie ma znaczenia cel integracji oraz przeznaczenie External Application. Zakładamy, że wszystkie operacje na zasobach w Business Service muszą zostać zaaplikowane w External Application i vice versa. Źródłem danych do synchronizacji może również być External Application. Przykładowo, jeśli aplikacje umożliwiają operację aktualizacji danych użytkownika, to aktualizacja w jednym z systemów musi ostatecznie zakończyć się aktualizacją danych w drugim.

W przypadku komunikacji Business Service -> External Application komunikacja odbywa się za pomocą REST API. Flow integracji rozpoczynający się w External Application wykorzystuje mechanizm webhooków. Business Service wystawia endpoint HTTP, na który External Application będzie wysyłał informacje o zdrarzeniach.

Z góry odrzuciłbym rozwiązanie polegające na bezpośrednim wykonywaniu zapytań z poziomu logiki biznesowej do External Application. Takie podejście wprowadza do kodu wysoki coupling między logiką związaną z integracją z External Application a logiką biznesową znajdującą się w Business Service. Wysoki coupling między tymi komponentami sprawia, że:

  • Ewentualna podmiana External Application na jakiekolwiek inne rozwiązanie staje się ogromnym problemem;
  • Czytelność i utrzymywalność takiego kodu drastycznie spada. Jakakolwiek zmiana w interfejsie API External Application powoduje konieczność ingerencji w kod biznesowy. Przenikanie się tych warstw kodu sprawia, że ryzyko potencjalnych błędów czy niespójności również rośnie.
  • Przenikanie się dotyczy również modeli danych z obydwu serwisów.

Propozycja rozwiązania

Planem minimum moim zdaniem jest wprowadzenie tu Adaptera w jego klasycznej postaci. Kod biznesowy korzystałby wtedy z dedykowanego klienta do komunikacji między logiką biznesową a External Application. Znacząco zredukuje to problem wysokiego couplingu. W przypadku aplikacji będącej typowym monolitem takie rozwiązanie może być nawet wystarczające.

Jednak w przypadku systemów rozproszonych czy modularnego monolitu warto rozważyć pójście krok dalej. Adapter można wyciągnąć do poziomu osobnej aplikacji. Na potrzeby artykułu nazwałem go Integration Service.

 

Adapter - diagram systemu

 

Rozwiązuje to kolejne kilka problemów, których nie rozwiązuje klasyczny adapter:

  • Business Service znika odpowiedzialność wystawienia endpointu na potrzeby mechanizmu webhooków;
  • Business Service nie ma odpowiedzialności translacji zdarzeń i obiektów z External Application na zrozumiałe przez Business Service;
  • Potencjalna podmiana External Service na inne rozwiązanie może odbywać się stopniowo. Można równolegle komunikować się z dwoma Integration Service z wykorzystaniem tego samego interfejsu wystawionym przez Business Service. W przypadku klasycznego adaptera albo wymagałoby to ingerencji w oryginalny adapter lub dostosowanie kodu biznesowego do pracy z dwoma adapterami. W wariancie z Integration Service możemy inkrementalnie budować drugi Integration Service i po ukończeniu wyłączyć ten pierwszy. Drugi Integration Service oczywiście korzysta z tego samego interfejsu co pierwszy.

W opisanym podejściu Business Service nie wie, absolutnie NIC o External Application. Dzięki temu moim zdaniem łatwiej jest upilnować, czy szczegóły związane z External Application nie ciekną do kodu biznesowego. Integration Service odciąża Business Service w następujących zadaniach:

  • Odbieranie komunikacji przychodzącej z External Application (webhooki) i ich translacja na zdarzenia zrozumiałe przez Business Service;
  • Mapowanie zasobów z External Application na zasoby z Business Service i vice versa. W Integration Service powinna znaleźć się mapa pozwalająca na dopasowanie zasobów z poszczególnych systemów. Business Service w komunikacji wychodzącej i przychodzącej powinien polegać wyłącznie na swoich identyfikatorach zasobów.
  • Translacja zdarzeń w Business Service na akcje/zapytania w External Application;

Integration Service w literaturze

Eric Evans w Domain-Driven Design opisuje przypadek, zbliżony do opisanego w artykule problemu:

Kiedy tworzony jest nowy system, który musi mieć duży interfejs z innym systemem, trudność powiązania dwóch model może ostatecznie przeważyć nad celem nowego modelu, zmuszając go do zmodyfikowania w sposób przypominający model innego systemu.

Evans opisał koncept pokrywający się z podejściem z Integration Service jako Anti-corruption Layer (ACL):

Utwórz warstwę izolującą, udostępniającą klientom funkcjonalność na warunkach ich własnego modelu dziedziny. Warstwa ta powinna komunikować się z innym systemem poprzez jego istniejący interfejs i nie powinna wymagać wcale lub tylko niewiele modyfikacji tamtego systemu. Wewnątrz warstwy zgodnie z potrzebami dokonywane  są tłumaczenia w obu kierunkach pomiędzy dwoma modelami.

Domain-Driven Design Evans opisał również inny przypadek użycia. Opisany został przykład integracji z naszym starym systemem. Leciwe systemy często mają problemy wynikające z ich długletniego rozwijania i popełnianych błędów. Nie powinny one wpływać na nowy system. W takim przypadku możemy potraktować go tak samo, jak External Application i schować za Integration Service (Anti-corruption Layer). Na ciekawy artykuł dotyczący ACL trafiłem również w zasobach Microsoft Learn.

Synchronicznie czy asynchronicznie?

Opisane rozwiązanie można przygotować zarówno w wariancie synchronicznym np. z wykorzystaniem HTTP, jak i asynchronicznym, wykorzystując komunikację zdarzeniami. Wariant synchroniczny jest o tyle problematyczny, że jest podany na stabilność zewnętrznego systemu i czas jego reakcji.

Moim zdaniem w opisanym podejściu lepiej sprawdzi się komunikacja oparta na zdarzeniach. W przypadku niedostępności External Application, zdarzenie konsumowane przez Integration Service wpadnie na Dead Letter Queue i będzie mogło zostać ponownie przetworzone, gdy External Application zacznie działać.

Business Service staje się również niezależny od czasów odpowiedzi z External Application. Ma to szczególne znaczenie, gdy użytkownik Business Service czeka na odpowiedź.

Przykładowo, w aplikacjach jest synchronizacja obiektów User. Użytkownik ma możliwość zmiany danych obiektu. W przypadku synchronicznym wygląda to następująco:

  1. Użytkownik wykonuje zapytanie HTTP do Business Service.
  2. Business Service wykonuje zapytanie HTTP do Integraton Service.
  3. Integration Service znajduje w bazie odpowiadający użytkownikowi identyfikator w External Application.
  4. W tym momencie może zadziać się wszystko. Możemy pomyślnie zaktualizować dane, ale możemy też np. czekać kilkanaście sekund na odpowiedź. Możemy też dostać błąd z aplikacji. Przypominam, że użytkownik cały czas czeka na odpowiedź.
  5. Integration Service wysyła zwrotkę do Business Service.
  6. Business Service aktualizuje dane użytkownika lub rzuca błędem.

W przypadku asynchronicznym wygląda to moim zdaniem znacznie prościej:

  1. Użytkownik wykonuje zapytanie HTTP do Business Service.
  2. Business Service emituje UserUpdatedEvent i zwraca status 200 użytkownikowi.
  3. Integration Service łapie UserUpdatedEvent.
  4. Integration Service znajduje w bazie odpowiadający użytkownikowi identyfikator w External Application.
  5. W tym kroku również może zadziać się wszystko. Jeśli update potrwa kilkanaście sekund to trudno. Użytkownik już ma zaktualizowane dane. Jeśli External Application rzuci błędem, to event trafi na Dead Letter Queue i będzie przeprocesowany później. Ostatecznie jednak prędzej lub później system będzie spójny.

Należy jednak wspomnieć, że nie da się 100% przypadków obsłużyć komunikacją asynchroniczną. Prędzej lub później trafi się taki przypadek, który będzie można obsłużyć wyłącznie synchronicznie. Przykładowo może to być sytuacja, gdy Business Service musi zwrócić informację, którą może pobrać wyłącznie z External Service.

Jeśli informacja ta jest nie dynamicznie zmieniającą się wartością np. zależną od podanych danych wejściowych lub od czasu wykonania, to dobrym pomysłem może być wprowadzenie warstwy cache. Ograniczy to liczbę zapytań do External Service. Wprowadzenie cache nie zmienia faktu, że w niektórych przypadkach skazani jesteśmy na komunikację synchroniczną i wynikające z niej trudności i ograniczenia.

Where wady?

Żeby ten artykuł nie brzmiał jak telezakupy, trzeba uczciwie wspomnieć o wadach omawianego rozwiązania. Najbardziej oczywistym minusem jest dodatkowy element systemu do utrzymania. Dedykowanemu serwisowi trzeba przydzielić zasoby, trzeba go obserwować (alerty, logi, metryki), reagować na jego awarie itd..

Drugi minus jest jednocześnie plusem. W Integration Service prawdopodobnie powstanie sporo ciężkiego w zrozumieniu i utrzymaniu kodu. Konieczność dopasowania się do zewnętrznego API, narzut związany z translacją i mapowaniem zasobów bardzo szybko może doprowadzić do powstania nieczytelnego spaghetti. Warto być tego świadom i od samego początku szczególnie zwracać uwagę na czytelność kodu w tym miejscu. Z drugiej jednak strony cały „smród” trzymamy w jednym miejscu w systemie, dzięki czemu nie przechodzi on w inne części systemu.

Trzeci minus to możliwość tymczasowej niespójności danych w jednym z systemów w podejściu asynchronicznym. Ostatecznie dane będą spójne. Jednak przez pewien czas jeden z systemów może polegać na przestarzałych danych.

Podsumowanie

O tym, że opisane przeze mnie rozwiązanie znane jest jako Anti-corruption Layer, prawdę mówiąc zorientowałem się, robiąc research na potrzeby tego artykułu. Niezależnie od tego, czy zapamiętasz je jako ACL, czy jako Integration Service, mam nadzieję, że będzie dla Ciebie przydatne i znajdziesz dla niego zastosowanie w swoich projektach. Zachęcam do podzielenia się w sekcji komentarzy swoimi doświadczeniami w integracji z zewnętrznymi systemami i wykorzystanych przez Ciebie rozwiązaniach.

Źródła i materiały dodatkowe

Dominik Szczepaniak

Zawodowo Senior Software Engineer w CKSource. Prywatnie bloger, fan włoskiej kuchni, miłośnik jazdy na rowerze i treningu siłowego.

Inne wpisy, które mogą Cię zainteresować

Zapisz się na mailing i odbierz e-booka

Odbierz darmowy egzemplarz e-booka 106 Pytań Rekrutacyjnych Junior JavaScript Developer i realnie zwiększ swoje szanse na rozmowie rekrutacyjnej! Będziesz też otrzymywać wartościowe treści i powiadomienia o nowych wpisach na skrzynkę e-mail.

Subscribe
Notify of
guest

0 komentarzy
Most Voted
Newest Oldest
Inline Feedbacks
View all comments