Oskar Dudycz

Pragmatic about programming

Prawda to, że zdarzenia powinny być jak najmniejsze?

2019-12-30 oskar dudyczEvent Sourcing

Cześć!

Jak tam Twoje Święta? Mam nadzieję, że spełniły się życzenia z zeszłego tygodnia i udało się bez programowania? U mnie się udało, kontynuuję jeszcze w tym tygodniu, w każdym razie do 2 stycznia, wtedy twarda rzeczywistość mnie dopadnie - powrót do roboty - na szczęście tylko na 2 dni, potem łikendzik. Głównie spędziłem czas na zabawie z córką, lekturze GoT. Znalazłem nawet czas na pogranie w Dungeon Keeper - ta gra po tylu latach jest ciągle niesamowicie grywalna. Jednym słowem - klawo! Jedyny minus, to brakujące miejsca przy stole, których coraz więcej z roku na rok, ale taka kolej rzeczy.

No ale dobra, to nie listy do Bravo, tylko Newsletter programistyczny! Do rzeczy!

Zatem, jak duże powinno być zdarzenie? Czy też może jakie informacje w nim przekazywać? Niestety nie powiem Ci złotego środka, bo takiego nie ma, ale postaram się przybliżyć kilka podstaowych zasad oraz moich przemyślen na ich temat.

Najpopularniejszym stwierdzeniem, że zdarzenie powinno być jak najmniejsze. Jest to z grubsza prawda, tylko co to znaczy “jak najmniejsze”? Na to jest już zdecydowanie trudniej odpowiedzieć. Zastanówmy się najpierw po co publikujemy zdarzenie. Tak jak pisałem już w poprzednich newsletterach zdarzenie jest to informacja o fakcie, który się wydarzył. Jest to nieco odwrócona komunikacja do tej, do której jesteśmy przyzwyczajeni. W klasyczynym API Restowym to zainteresowany klient musi nas zapytać o interesującą go informację, podczas gdy publikując zdarzenie to my informujemy wszystkich zainteresowanych. W zasadzie to nawet nie wiemy czy ktoś jest w ogóle zainteresowany. Zakładamy, że chcielibyśmy, żeby ktoś te zdarzenie obsłużył, ale co zrobi z tą informacją? Tego już nie wiemy.

Jeżeli spojrzymy na nasze API webowe z boku, to sytuacja wcale nie jest taka dalece inna. Jeżeli wybieramy kierunek tzw. “API first” to wtedy tak naprawdę my określamy to jak chcielibyśmy aby nasi klienci z niego korzystali. API jest naszym punktem wyjścia i naszym głównym produktem. Klienci muszą się dopasować. W innym podejściu - tzw. “API for frontend” API wystawia takie endpointy, które są dopasowane do potrzeb klientów. W takim podejściu wygoda klienta jest najważniejsza i to aby on mógł jak najbardziej efektywnie korzystać z naszych endpointów. Jedno i drugie podejście ma swoje wady i zalety. “API first” zwykle jest bardziej spójne, bardziej organiczne, ale za to może powodować utrudnienia dla klienta jeśli on ma znacząco inne potrzeby niż założenia takiego api. “API for frontend” z kolei pozwala klientom pracować efektywniej, ale zrzuca więcej na API, często mają one duplikację danych i trudniej zachować spójną wizję. Więcej o tym możesz przeczytać w świetnym artykule Jimmy’ego Bogarda “Composite UIs for Microservices: Vertical Slice APIs”.

Dlaczego piszę o Web Api gdy powinienem pisać o zdarzeniach? Bo tworząc system oparty o zdarzeniach nie unikniemy wcale tych dylematów. Weźmy np. ten wyświechtany przykład Zamówień. Po ostatecznym potwierdzeniu zamówienia moduł finansowy powinien wystawić fakturę, moduł obsługi przesyłek nadać ją, moduł powiadomień powinien wysłać emaila. Możemy zatem zdefiniować sobie zdarzenie OrderConfirmed, które będzie miało wszystkie informacje zebrane na temat zamówienia w trakcie jego przetwarzania czyli dane osobowe kupującego, adres, kwotę i szczegóły zamówienia. Z tym, że może się okazać, że moduł obsługi przesyłek nie potrzebuje szczegółowych danych finansowych tylko dane osobowe i dane adresowe. Moduł finansowy nie potrzebuje danych adresowych do przesyłki tylko dane firmy (które mogą się różnić). Moduł powiadomień z kolei nie powinno nic wiedzieć na temat kupującego poza jego imieniem i mailem, bo mail jedyne co będzie miał to link do strony z zamówieniami. Nie mówiąc nawet o RODO…

Może się zatem okazać, że zdarzenie OrderConfirmed w takiej skomasowanej formie będzie miało dużo danych nadmiarowych, które (już wspominająć o RODO) mogą być nawet niepożądane do wysyłania wszędzie. Można zatem zamiast jednego zdarzenia puścić 3: OrderConfirmed z podstawowymi danymi zamówienia, OrderReadyForShipment z danymi dla modułu obsługi przesyłek, OrderPaid z informacjami finansowymi. Dzięki temu każdy z modułów będzie mógł nasłuchiwać na konkretne zdarzenie z interesującymi go informacjami. Podobnie jak z “API for frontend” może to powodować duplikację danych i nieco większy koszt utrzymania, ale w konkretnych sytuacjach może być to dużo lepsze rozwiązanie niż jedno zdarzenie, które złączy wszystko. Ryzyko jest też takie, że de facto tworzymy większy coupling między serwisami, bo musimy wiedzieć czego dokładnie inne moduły potrzebują i to nasz moduł musi się dopasowywać.

Często spotykanym błędem jest literalne traktowanie zasady, że zdarzenia powinny być jak najbardziej granularne. Faktycznie powinny one odzwierciedlać zdarzenia biznesowe, ale może to powodować nieprzewidziane na początku problemy. Wróćmy do naszego przykładu zamówienia. Każda z operacji “dodano do koszyka”, “przypięto koszyk do użytkownika”, “wybrano adres dostawy”, “wybrano adres do dostawy”, ” potwierdzono dostępność produktów” wyzwoli w module zamówień zdarzenie (szczególnie jeśli jest robiony zgodnie z Event Sourcing). Każde z tych zdarzeń jest poprawne i ma znaczenie w ramach modułu zamówień, ale najgorsze co moglibyśmy zrobić to kazać modułom zewnętrznym “weź sobie dane użytkownika z BucketAssignedToUser, dane produktów z ProductAddedToBucket, dane adresowe z DeliveryAddressSelected). Zewnętrzne moduły w naszym przykładzie nie interesuje wewnętrzny proces zamówienia tylko ostateczny jego kształt. Nakazując modułom nasłuchiwać na wszystkie możliwe zdarzenia, które z ich perspektywy nie mają żadnego znaczenia biznesowego to wyciek logiki biznesowej. To pierwszy krok do rozproszonego monolitu. Jeżeli, moduł finansowy nie będzie wiedział, że pojawiło się np. zdarzenie ProductQuantityUpdated, które się dzieje jeszcze zanim zamówienie zostało złożone to nie wygeneruje poprawnych danych do faktury. Powoduje to, że moduł finansowy musi znać szczegółowo proces biznesowy zamówień i co gorsza na jego zmiany reagować. Co chyba przyznasz, że nie brzmi najlepiej?

Ja lubię dzielić zdarzenia na wewnętrzne i zewnętrzne. Wewnętrzne to takie, które są zrozumiałe i poprawne w kontekście wewnątrzmodułowym. Zewnętrzne to takie, które są zrozumiałe w kontekście całego systemu i ogólnego procesu. Czy zdarzenie może być jednocześnie wewnętrzne i zewnętrzne? Oczywiście - choćby przywołane wcześniej OrderConfirmed. Jeśli jednak mamy 10 zdarzeń które zmieniają status zamówienia, a inne moduły są zainteresowane tylko informacją o zmianie statusu to możemy zrobić wewnętrzny Event Handler, który będzie na nie nasłuchiwał, mapował na zewnętrzne zdarzenie OrderStatusChanged i publikował je na zewnątrz.

Nie ma zatem najlepszej odpowiedzi. Jak zwykle jest nią “to zależy”.

Mogę też odpowiedzieć śpiewająco: “Wlazł kotek na płotek i mruga. Piękna to piosenka, niedługa. (…) Niedługa, niekrótka, a w sam raz. Zaśpiewaj koteczku jeszcze raz”.

Zatem nasze zdarzenia powinny być niedługie i niekrótkie. W ram raz. Przy ich projektowaniu zachowajmy zdrowy pragmatyzm. Traktujmy nasze zdarzenia nie jako element techniczny ale biznesowy.

Jako API naszego modułu.

Co o tym sądzisz?

Pozdrawiam i życzę Ci już teraz wszystkiego najlepszego w kolejnym roku!

(Ja mam nadzieję, że jutro na Sylwka dotrwamy z moją żoną do północy po uśpieniu naszej Młodej).

  • © Oskar Dudycz 2019-2020