Cześć! Minął weekend, mój tygodniowy urlop dobiegł końca. Nadszedł też dzień, w którym pojawi się kolejny wpis! Dziś będzie solidna i skondensowana dawka wiedzy. Od początku wiedziałem, że tematu architektury nie może zabraknąć w Cesarstwie-Dev. Długo myślałem o tym od jakiego tematu rozpocząć wpisy o tej tematyce. Na szczęście odpowiedź przyszła do mnie sama, podczas czytania pewnej książki. Książka ta najpierw przedstawiła mi listę pewnych rzeczy, a następnie skupiła się na opisaniu każdej z osobna. To jest dobry schemat. W dzisiejszym wpisie poznamy architekturę warstwową, heksagonalną, wzorzec CQRS oraz architekturę sterowana zdarzeniami (EDA – Event-driven Architecture). Kończąc przydługi wstęp! Zapraszam Was serdecznie na pierwszy wpis na temat architektury systemów, który poświęcony będzie krótkiemu opisaniu najbardziej popularnych architektur. Gotowi? To zaczynamy!

Dla kogo?

Wpis ten jest zdecydowanie skierowany do wszystkich! Osoby niedoświadczone poznają nowe pojęcia, dowiedzą się czym jest architektura oraz dostaną solidny zastrzyk informacji. Starzy wyjadacze natomiast dokonają pewnej powtórki wiedzy, a może też dowiedzą się czegoś nowego. Na pewno nikt nie będzie się nudził.

Wstęp

Architektura systemu to kształt, który nadają mu wszystkie pracujące nad nim osoby. Kształt systemu wynika z podział kodu na komponenty, ułożenia tych komponentów oraz sposobów, w jakie one się ze sobą komunikują.

Robert C. Martin

W książce o tytule „Czysta Architektura” Robert C. Martin opisuje architekturę w powyższy sposób. W tej samej książce ujęta jest również inna wspaniała myśl, która sugeruje, że celem architektury jest jak najdłuższe odwlekanie w czasie podejmowania technicznych decyzji. Wpis na ten temat na pewno pojawi się w Cesarswie-Dev!

Nie chcąc konkurować z autorem wielu wspaniałych książek, nie będę próbował ubierać w słowa własnej definicji architektury. Skupię się więc na liście, która zawierać będzie architektury oraz wzorce i style architektoniczne. Styl architektoniczny jest to sposób implementacji określonej architektury, natomiast wzorzec architektoniczny rozwiązuje pewien konkretny problem w zadanej architekturze. Dodatkowo, wzorzec architektoniczny jest szerszy od wzorca projektowego (odsyłam do krótkiego wpisu o wzorcach projektowych).

Architektura warstwowa

Znana i lubiana! Zobaczmy jak prezentuje się ta architektura na schemacie.

Umieszczona na powyższym schemacie architektura zakłada najczystsze podejście do warstw. Dzieli ona system na cztery warstwy oraz, korzystając z zasady odwrócenia zależności, ustala w odpowiedni sposób komunikacje miedzy warstwami. Zasada odwrócenia zależności (ang. Dependency Inversion Principle) mówi, że moduły wysokiego poziomu nie powinny zależeć od modułów niskiego poziomu. Warto zwrócić uwagę, że podkreślone zostało znaczenie tego, by warstwa usług niskopoziomowych (infrastruktury) zależała od interfejsów zdefiniowanych przez komponenty wyższego poziomu. Przejdźmy teraz do opisu warstw!

  1. Warstwa domeny – najważniejsza warstwa naszej aplikacji. Tutaj umieszczone są encje i agregaty zaimplementowane zgodnie z DDD (tutaj opisane przez wspaniałego Martina Fowler’a). W tym miejscu znajduje się logika biznesowa naszej aplikacji. Warto podkreślić że warstwa domeny nie zależy od niczego. Jest czysta.
  2. Warstwa aplikacji – Tutaj należy określić w jaki sposób można komunikować się z agregatami zdefiniowanymi w warstwie domeny. Znajdujące się tutaj usługi aplikacji, choć pozbawione logiki biznesowej, spełniają bardzo ważna rolę w funkcjonowaniu systemu. Mogą mieć wiele zastosowań. Od wysyłki powiadomień do innych systemów na bazie zdarzeń domenowych, do bycia bezpośrednim klientem agregatów domenowych. Usługi te są często operacjami koordynującymi, które wywołują odpowiednie metody na obiektach dziedziny. Osobiście lubię nazywać je przypadkami użycia (ang. UseCase). Przykładem takiego przypadku użycia będzie wyciągnięcie egzemplarza agregatu z sytemu persystencji (np. bazy danych) za pomocą zdefiniowanego w domenie interfejsu repozytorium, oraz wykonanie na nim jakiegoś polecenia.
  1. Warstwa interfejsu użytkownika – zawiera wyłącznie kod związany z obsługą żądań, które może wykonać użytkownik, oraz, w razie potrzeby, kod warstwy wizualnej.
  2. Warstwa infrastruktury – najniższa warstwa naszego systemu. Tutaj znajdują się mechanizmy persystencji danych, wysyłki wiadomości email, integracja z zewnętrznymi systemami, czy obsługa systemu kolejkowego (np. RabbitMQ). Wszystkie niskopoziomowe zależności znajdziemy tutaj.

Architektura heksagonalna

Architektura ta jest znana także jako sześciokątna, jak i architektura portów i adapterów. Powstała ona w celu ułatwienia obsługi licznych formatów danych wejściowych, uproszczenia ich podmiany oraz zmaksymalizowania modułowości (a więc i wymienności) adapterów wyjściowych. Wiele zespołów, które mówi że korzysta z architektury warstwowej, tak naprawdę nieświadomie korzysta z architektury heksagonalnej. Koncentruje się ona na modelu dziedziny, który otoczony jest przez warstwę aplikacji. Stanowi to część wewnętrzną tej architektury. Część zewnętrzną stanowią adaptery oraz porty. Dla ułatwienia wyobrażenia sobie tej architektury potem przykładowe adaptery i porty:

  • Port obsługujący odbieranie wiadomości z systemu RabbitMQ oraz adapter zmieniający otrzymane dane na modele znane w warstwach wewnętrznych
  • Port obsługujący żądania HTTP oraz adapter, który przekształca dane zwrócone z aplikacji na odpowiedni model wyjściowy
źródło: https://medium.com/swlh/hexagonal-architecture-in-java-b980bfc07366

Architektura ta wywiera mocny wpływ na organizację kodu w warstwie aplikacji. Granicami wewnętrznego sześciokąta są właśnie przypadki użycia (usługi aplikacji). W chwili gdy system otrzymuje żądanie przez któryś z portów wejściowych, to adapter przekształca te dane na odpowiedni format oraz korzysta z odpowiedniego przypadku użycia. Przypadek ten może skorzystać wewnętrznie z portu wyjściowego do bazy danych (poprzez adapter wyjściowy) w celu zachowania zmian. Jak widać na schemacie powyżej wybranie takiej architektury ułatwia nie tylko modularność kodu (na przykładzie dwóch różnych adapterów bazy danych), jak i wspiera korzystanie z licznych portów wejściowych wywołujących ten sam przypadek biznesowy z warstwy aplikacji. Tak projektowany system ukierunkowany jest na spełnianie wymagań funkcjonalnych.

CQRS

Przed nami pierwszy wzorzec architektoniczny! Na początku wpisu wspomniałem, że wzorce powstały by rozwiązywać konkretny problem. Od tego też zacznijmy opis CQRS’a! Odpytywanie bazy danych o wszystkie dane, jakie użytkownik potrzebuje do uzupełniania wybranego widoku potrafi być czasochłonne. Widoki te często składają się z danych pochodzących z różnych agregatów czy tabel. Im bardziej skomplikowana jest domena naszego systemu, tym mocniej odczujemy tu problem wydajnościowy.

Na szczęście wspomniany wzorzec powstał w celu rozwiązania tego zagadnienia. Rozszyfrujmy wpierw nazwę. Command Query Responsibility Segregation – Segregacja odpowiedzialności Polecenie-Zapytanie. Te dosłowne tłumaczenie wspaniale opisuje ideę naszego wzorca – podzielenie implementacji na dwie części. Część Command będzie zawierała wszelkie agregaty, które zawierać będą wyłącznie metody modyfikujące ich stan, lecz nie będą już zawierać żadnych getterów. Cała logika domenowa będzie ukryta w tej warstwie. Ta część będzie również zawierała odświeżenie modeli widoku po aktualizacji agregatu. Proces ten nie musi odbywać się bezpośrednio w komponencie Command, gdyż może on wyglądać jak na schemacie poniżej. Część Query naszego systemu jest bardzo prosta – pobiera ona wyłącznie potrzebne modele widoku z swojego magazynu. Warstwo zwrócić uwagę na to, że modele w części zapytań są to zdenormalizowane obiekty domenowe. Nie odzwierciedlają one bezpośrednio encji czy agregatów, lecz dostosowane są pod konkretne potrzeby widoków użytkowników.

Schemat ten przedstawia wersję CQRS korzystająca z dwóch baz danych. Możliwa jest również implementacja na jednej bazie zawierającej osobne tabele dla modelu poleceń oraz widoku. Serdecznie polecam podcast na temat tego wzorca, gdzie również jest przedstawiony CQRS na jednej bazie (tak zwany „CQRS po białostocku”). Jakie plusy daje nam CQRS? Na pierwszy plan idzie tutaj wydajność oraz możliwość osobnego skalowania obu części systemu. Dodatkowo, pomaga wydzielić odpowiednio zadania do członków zespołu – mniej doświadczone osoby na pewno nie pogardzą zadaniami z części zapytań, natomiast weterani będą zgrzytać zębami nad kolejnymi zagwozdkami części poleceń.

Architektura sterowana zdarzeniami

Na sam koniec zostawiłem architekturę sterowaną zdarzeniami, która nieprzypadkowo znalazła się właśnie w tym miejscu. Świetna ona łączy się z wyżej wymienioną architekturą heksagonalną, a dokładniej potrafi ona połączyć serwisy zaimplementowane w tym stylu. Jest bardzo wiele definicji tej architektury, więc zdecydowanie nie należy przywiązywać się do żadnej. Treść tego akapitu nie należy traktować jako jedynej prawdziwej definicji, lecz przykładu działania tak zaprojektowanego systemu. EDA jest to architektura, która promuje tworzenie oraz wysyłanie wydarzeń, a także reagowanie na te, które interesują dany komponent. Spójrzmy na schemat!

Trójkąty powyższego schematu symbolizują zdarzenia. Heksagony natomiast przedstawiają różne aplikacje napisane w architekturze sześciokątów. Do każdego systemu mogą wpływać, za pomocą portów, zdarzenia do których dany system się zasubskrybował. Rozważmy tutaj przykład. Posłużmy się najbardziej rozpowszechnionym w teoretycznych rozważaniach systemem eCommerce. Załóżmy, że posiadamy trzy mikroserwisy: zamówienia (system 2), płatności (system 3) oraz magazyn (system 1). Powyższy schemat może przedstawiać proces zakupu pewnych towarów. Przejdźmy to krokami

  1. Użytkownik używa serwisu zamówień w celu złożenia zamówienia
  2. Serwis zamówień obsługuje zgłoszenie oraz wysyła zdarzenie: UtworzonoZamówienie
  3. Serwis płatności, który subskrybuje to wydarzenie, odbiera je i przygotowuje proces płatności dla tego użytkownika
  4. Użytkownik używa serwisu płatności w celu opłaty zamówienia
  5. Serwis płatności obsługuje żądanie. W przypadku sukcesu wysyła zdarzenie OtrzymanoPłatność
  6. System zamówień subskrybuje to wydarzenie, więc odbiera i zmienia status zamówienia na opłacony
  7. System magazynu również subskrybuje to wydarzenie i także je odbiera. Następnie rozpoczyna proces wysyłki

Powyższy proces może być kontynuowany poprzez kolejne zdarzenia np. WysłanoZamówienie, jednak zdecydowałem się go skrócić, gdyż powyższy przykład w zupełności wystarcza do zrozumienia idei.

Architektura ta pozwala na łatwą komunikację pomiędzy różnymi serwisami naszego systemu i także pozwala w łatwy sposób dodawać nowe funkcjonalności. Kolejnym autem tego podejścia jest ułatwienie rozmów z ekspertami dziedziny, gdyż to wraz z nimi przygotowujemy nazwy zdarzeń. Oni również pomagają ustalić które serwisy powinny interesować się konkretnymi zdarzeniami. Takie podejście znacząco ułatwia proces projektowania oraz implementacji. Skrócenie dystansu między technologią a biznesem potrafi mieć ogromny wpływ na jakość tworzonego produktu. Poszczególne usługi systemu przedstawione są jako heksagony, jednak mogą być równie zastąpione architekturą warstwową, bądź dowolnym innym stylem.

To jest już koniec?

Istnieje jeszcze wiele rozwiązań, o których nie wspominam w tym artykule. Zdecydowanie brakuje jeszcze sekcji o Event Sourcing’u, potokach i filtrach czy REST’cie. Na szczęście nie jest to ostatni wpis na tym blogu! Na pewno pojawi się kolejna część tego cyklu, jednak raczej nie nastąpi to szybko. W kolejnych wpisach, które dotyczyć będą architektury, będę chciał skupić się na uszczegółowieniu konkretnej architektury wraz z przykładami w .NET’cie. Możecie pomóc mi zdecydować od czego zacząć w ankiecie na dole wpisu!

To już koniec naszej dzisiejszej wędrówki po kilku popularnych architekturach. Zdecydowanie wpis ten jest mieszanką wybuchową. Zawiera liczne elementy które pomagają tworzyć dobre i niezawodne oprogramowanie. A przecież o to chodzi, prawda? Mam nadzieję, że wpis Wam się podobał. Dajcie znać w komentarzu! Serdecznie Was również zapraszam do przeczytania innych wpisów w Cesarstwie-Dev. Wiecie już jak poprawnie dostarczyć klienta API, tak by każdy w zespole mógł korzystać z niego prawidłowo? Nie? To koniecznie zajrzyj tutaj! Nie chcąc już przedłużać, dziękuję za uwagę i do napisania!

Jak widać, powyższa ankieta została już zakończona. Jeśli jesteście ciekawi wyników tej ankiety to zapraszam Was do kolejnego wpisu.


3 Komentarze

Marcin · 2020-11-17 o 15:14

Fajny wpis ale no suche to trochę. Najlepsze jest mięcho i żywe przykłady. Wiadomo że teori

    Cesarz · 2020-11-17 o 15:20

    Cześć Marcin!
    Dzięki za komentarz! Masz rację, że wpis ten jest pozbawiony przykładów. Jest to spowodowane faktem, że strasznie wydłużyłaby się jego treść gdybym chciał objąć cały materiał. Jest to wyłącznie pierwszy wpis z serii architektury – zawierający jak najogólniejszy opis różnych architektur. Przykłady możesz znaleźć w kolejnych wpisach. Można już znaleźć wpis opisujący architekturę heksagonalną oraz CQRS.
    Pozdrawiam!

dotnetomaniak.pl · 2020-09-23 o 08:26

Przebieżka po architekturach

Dziękujemy za dodanie artykułu – Trackback z dotnetomaniak.pl

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *