Ta „frontalna” funkcja wtedy subskrybuje wiele topicow i odpala dalej różne funkcje? To nie jest problem, ze tworząc nowa funkcje zawsze tam tez trzeba dodać „routing” (więcej kodowania i zużycia zasobów)? ;)
Nie, niejasno opisałem o co konkretnie chodzi. Pisząc "routing" miałem na myśli routing na poziomie aplikacji, nie "fizycznej" infrastruktury.
Pozwól że po staram się pokrótce opisać jak to u nas wygląda z poziomu architektury ale i pewnej filozofii pisania oprogramowania. Mamy system oparty o mikroserwisy i event-driven, a więc komunikacja pomiędzy serwisami odbywa się właśnie na takiej zasadzie że jeden serwis publikuje event, i inne serwisy mogą nasłuchiwać tego eventu i odpowiednio na niego reagować. Na popzimie infrastruktury używamy Azure ServiceBus, wszystko leci do jednego topica. Każdy serwis ma swój proces nasłuchujący na tym topicu, i to subskrypcja danego serwisu definiuje jakie eventy się dostają do niego. Z perspektywy "filozofii" pisania nowych funkcjonalności, to wszystko to jest transparentne dla programistów. Jeśli programista pisze kod który musi nasłuchiwać danego eventu i go obsłużyć, to musi zrobić jedynie dwie rzeczy:
-
Zaimplementować interfejs IEventHandler<T>
gdzie T
jest konkretnym,silnie typowanym event domenowym. Taki event handler to po prostu jednostka logiki biznesowej, może dodatkowo przyjmować inne serwisy domenowe przez DI. Event handlery należą do warstwy domeny (bo mają logikę biznesową i operują na eventach domenowych), z punktu widzenia onion architecture są więc w corze systemu.
-
Nowy event musi zostać zarejestrowany, tak aby proces odbierający ten event z ServiceBusa wiedział gdzie go przekierować w kodzie aplikacji. Programista robi to w kodzie boostrapującym aplikację, wykorzystując do tego naszą warstwę infrastruktury (chodzi o infrastrukturę w kodzie, nie fizyczną). Mamy własną paczkę Messaging
która dostarcza takie funkcjonalności, więc wygląda to mniej więcej tak jak poniżej (nie dokładnie tak, ale to tylko przykład):
// Rejestracja DI
services.AddServiceBusEventListeners(config =>
config.UseConnectionString("")
.UseSubscriptionName("")
.Handle<MyEvent, MyEventHandler>() // To jest ten "routing"- mówimy do jakiego handlera skierować dany event
);
To jedyne co programista musi zrobić. Idea jest taka aby programista mógł skupić się na tym co naprawdę ma znaczenie- na logice biznesowej- i miał jak najmniej dodatkowej pracy. Całą resztą zajmuje się kod w naszej bibliotece od messagingu.
Za kulisami to wygląda tak że DI rejestruje kilka dodatkowych serwisów. Wtedy aplikacja hostująca ma podpięty moduł odpowiadający za odbiór eventów przychodzących z ServiceBus- to po prostu klasa która oznaczona jest atrybutem zawierającym trigger z ServiceBus. Moduł ten przyjmuje przez konstruktor procesor eventów- ten procesor właśnie odpowiada za zdeserializowanie surowych wiadomości z ServiceBus (obiekty typu Message
) na konkretny, silnie typowany event, oraz przekazanie go do jednego lub więcej event handlerów zarejestrowanych wcześniej (wymieniony wcześniej routing). I to są te dwa kluczowe elementy stwarzające pewien problem- moduł odbierający surowe wiadomości, i cała podpięta logika obsługująca je odpowiednio.
Bo gdybyśmy chcieli to robić "czysto" w rozumieniu serverless, to każdy event handler musiałby mieć oddzielny, jawny "moduł" odbierający dany event, na danym topicu- czyli funkcję. Rzecz w tym że ze względu na to iż mamy architekturę event-driven, tych handlerów mamy całkiem sporo. Gdyby dla każdego programista musiał tworzyć oddzielne funkcje, to kolidowało by to z naszą filozofią ułatwiania programistom życia, oraz skupianiu się na warstwie domeny. Dodatkowo, u nas wszystkie eventy idą po jednym topicu, co tym bardziej kolidowałoby z koniecznością wydzielania tego dla każdego rodzaju eventu. Musielibyśmy "wycinać" te zależności które dla konkretnego event handlera są niepotrzebne, zamiast po prostu rejestrować wszystko co potrzebne do wykonania przez logikę biznesową jako całość.
Co za tym idzie w naszym przypadku funkcje (w Azure) wyglądają tak że mamy tak naprawdę jedną funkcję dla jednego mikroserwisu, przyjmuje ona wiadomość z ServiceBus, a następnie przekazuje ją do naszego procesora który zajmuje się całym routingiem na poziomie aplikacji (w procesie) tak jak opisałem to wyżej. Taka funkcja nie jest więc tak "lekka" jak powinna, ale z drugiej strony nadal mamy łatwość skalowania zapewnioną przez Azure Functions.
Mam nadzieję że to co napisałem ma chociaż trochę sensu, zdając sobie sprawę że po części opisywałem to dosyć abstrakcyjnie :P