Cześć! Witam Was w kolejnym już wpisie w Cesarstwie-Dev. Będzie on w pewnym sensie wyjątkowy. Po raz pierwszy to Wy zdecydowaliście, o czym mam pisać! Bardzo się cieszę, że pewna część z Was zdecydowała się wziąć udział w ankiecie załączonej do ostatniego wpisu. Aż 18 głosów niezmiernie mnie ucieszyło, tym bardziej że tylko jeden głos pochodził od moich znajomych. Zwycięzcę ankiety już znacie. Jest nim architektura heksagonalna. Przedstawię jednak także pełne wyniki. Oto one:

Przejdźmy już do mięska! Po pierwsze, jeśli jeszcze nie przeczytałeś artykułu wprowadzającego do tematyki dzisiejszego tekstu to zapraszam Cię tutaj. A teraz nie pozostaje mi nic innego niż… zacząć!

Wstęp

Architektura heksagonalna jest znana pod kilkoma nazwami. Na szczęście, niezależnie od nazwy, w każdym opisie cechuje się takimi samymi rzeczami. Centrum tak napisanego systemu tworzy domena – czysta, testowalna i dobrze zaprojektowana. W tym może nam pomóc między innymi DDD, o którym na pewno pojawi się wpis w Cesarstwie-Dev. Aktualnie odsyłam do książki, po którą najczęściej sięgam na półkę. Komunikację z domeną zapewnia nam warstwa aplikacji, która udostępnia swoje usługi. Usługi aplikacji w tym wpisie nazywać będę przypadkami użycia (UseCase’ami). Posługiwanie się taką terminologią wspomaga proces komunikacji z biznesem, gdyż często pokrywają się one z wymaganiami funkcjonalnymi. Wiemy już co siedzi w środku naszego systemu. Przyjrzyjmy się teraz zewnętrznemu sześcianowi!

źródło: https://bulldogjob.pl/news/1145-architektura-heksagonalna-w-javie-w-3-minuty

Jak widać warstwę aplikacji otaczają porty, które są punktami komunikacji naszego systemu z zewnętrznymi. Wśród nich wyróżniamy zarówno porty wejściowe (np. Web API), jak i wyjściowe (np. baza danych). Porty są pewną abstrakcją, która wyznaczana jest przez środkowy sześciokąt, natomiast implementowana przez zewnętrzny. Wielkim atutem tej architektury jest wymienność adapterów. W początkowej fazie projektu możemy spokojnie używać adaptera, który w celu zapisania danych będzie korzystał z systemu plików. Odkładanie w czasie decyzji o technicznych szczegółach, takich jak na przykład system persystencji, jest bardzo ważne w aspekcie projektowania aplikacji. Dłuższy czas spędzony na pracy nad projektem pozwala nam lepiej zrozumieć różne jego aspekty oraz wymagania, co przekłada się na lepsze decyzje. Wymienność adapterów pozwala również na korzystanie z wielu adapterów wejściowych, które posiadają wspólną logikę biznesową. W związku z tym dodanie nowego adaptera wejściowego polega wyłącznie na dodaniu technicznego szczegółu, bez wpływu na istniejącą logikę biznesową.

W kolejnych częściach artykułu skupię się na implementacji przykładowego projektu z użyciem architektury heksagonalnej. Zdecydowanie nie jest to jedyny sposób wykorzystania tej architektury, jednak zdecydowanie wykorzystuje największe jej atuty, czyli wymienność adapterów, separację logiki biznesowej, jak i ukierunkowanie systemu na spełnianie wymagań funkcjonalnych.

Problem

Na początku zdefiniujmy problem, który postaramy się rozwiązać. Do stworzenia mamy system wspierający pisanie blogów (bo mamy już dość aplikacji eCommerce, prawda?). Na potrzeby wpisu nazwijmy go Blogging. Główną wartością biznesową, jaką powinien wnieść implementowany system, ma zostać zautomatyzowany oraz sformalizowany proces powstawania wpisów. System powinien również automatycznie publikować powiadomienia na odpowiednich kanałach w mediach społecznościowych. Co więcej, w zależności od autora wpisu, powiadomienie powinno być publikowane tylko w wybranych kanałach. Nie są to najbardziej szczegółowo opisane wymagania na świecie, jednak nie z jednego chleba jedliśmy piec. Często klient nawet tyle nam nie powie, a więc na potrzeby tego wpisu zdecydowanie wystarczy. Dziś skupimy się na funkcjonalności związanej z publikowaniem wpisu. To co? Zaczynajmy!

Struktura projektu

Na początku spójrzmy na nasz system z wysokiego poziomu. Podział systemu na wyspecjalizowane projekty oraz odpowiednie ustawienie zależności między nimi zdecydowanie ułatwia poprawne implementowanie aplikacji. Przejrzyjmy schemat zależności między projektami:

Przyjrzyjmy się istniejącym projektom:

  1. Blogging.Domain – zawiera domenę, logikę biznesową naszego systemu. Nie zależy od niczego
  2. Blogging.Application – zawiera przypadki użycia naszego systemu, zależy wyłącznie od domeny
  3. Blogging.Api – zawiera kod związany z dostępem do naszego systemu poprzez Web API, a więc jest to adapter wejściowy
  4. Blogging.Functions – kolejny adapter wejściowy. Jego implementacja oparta jest o Azure Functions
  5. Blogging.Infrastructure.Persistence – projekt zawierający adaptery wyjściowe, które związane są z utrwalaniem danych
  6. Blogging.Infrastructure.SocialMedia – projekt zawierający kolejne adaptery wyjściowe. Zawarte w nim klasy używane są w celu publikacji postów w mediach społecznościowych

Znamy już strukturę naszego projektu, która powstała w wyniku użycia architektury heksagonalnej. Przejdźmy więc do głównej części programu – kodu!

Sześciokąt wewnętrzny

Implementację rozpoczniemy od zaprojektowania klasy wpisu. Każda zaprojektowana w domenie klasa powinna składać się z danych oraz zachowania. Zacznijmy od danych:

I płynnie przejdźmy do zachowania:

Jak widać zaprojektowany agregat ma dość proste zachowanie. Pilnuje on stanu danego wpisu, jak i zarządza jego zawartością. W domenie przedstawione są również klasy odpowiadające za proces komunikacji z mediami społecznościowymi. Z uwagi na ich prostą implementację (z uwagi na skupienie na publikacji wpisów zostały pominięte pewne ich zachowania) nie zostaną one zawarte w tym wpisie. Przejdźmy do bardziej interesujących elementów! W środkowym sześciokącie, poza wartością biznesową, chcemy również zdefiniować porty wyjściowe. Spójrzmy na interfejsy, które zostaną zaimplementowane w innej części projektu.

Dodatkowo, w tym sześciokącie znajduje się również warstwa aplikacji. Zaprojektowany został przypadek biznesowy, który polega na publikacji wpisu. Przejdźmy do jego implementacji!

Powyższy przypadek biznesowy przyjmuje w konstruktorze interfejsy, które zostały zdefiniowane w warstwie domeny. Dzięki takiemu podejściu warstwa aplikacji nie posiada żadnych zależności zewnętrznych. Dodatkowo znacząco ułatwia to proces testowania jednostkowego, gdyż pozwala to na wykorzystanie atrap interfejsów. Klasa ta posiada wyłącznie jedną metodę, która opisuje konkretny przypadek użycia. Na początku pobieram post z systemu persystencji. Następnie wywołuję odpowiednią metodę agregatu Post w celu odpowiedniego ustawienia stanu wpisu. W kolejnym kroku pobieram definicje kanałów społecznościowych dla danego autora, by następnie opublikować post za pomocą portów wyjściowych. Na sam koniec zmiany w agregacie utrwalam w systemie persystencji. Proste, prawda?

Sześciokąt zewnętrzny

Po implementacji wewnętrznego sześciokąta możemy skupić się na zależnościach zewnętrznych. Zaczniemy od najpopularniejszego rodzaju adapterów wyjściowych – prowadzącego prosto do systemu persystencji. Skupimy się na implementacji repozytorium postów. Niemniej, nie będzie to typowa implementacja z użyciem Entitiy Framework’a. Zaczniemy od implementacji służącej krótkim testom systemu:

By następnie przejść do implementacji korzystającej z REST’owego systemu persystencji, który wystawiony jest pod znanym adresem URL:

Warto zauważyć, że obie te klasy nie są publiczne. Widoczne są wyłącznie w zakresie konkretnego projektu, w którym są implementowane. Odwołania do powyższych poza obrębem własnego projektu możliwe są wyłącznie poprzez interfejs zdefiniowany w warstwie wysokopoziomowej. Jak pięknie tu widać zasadę odwrócenia zależności! Przypomnijmy sobie:

 High-level modules should not depend on low-level modules. Both should depend on abstractions.

Robert C. Martin

Wróćmy jednak do implementacji! Warto zapewnić naszej aplikacji również punkty wejściowe. Jednym z nich może być kontroler ASP.NET Core Web Api. Spójrzmy na przykładową implementację:

Wielką zaletą wykorzystywania wspaniałego konceptu, jakim jest architektura heksagonalna, jest możliwość wykorzystania tego samego przypadku użycia w kilku adapterach wejściowych. Wyobraźmy sobie, że nasz system integruje się z inną aplikacją. W sytuacji opublikowania wpisu w drugim systemie nasza aplikacja powinna również opublikować ten sam wpis (zakładając, że wpisy w obu systemach są synchronizowane). Ten zewnętrzny system proponuje wysłanie nam odpowiedniej wiadomości na kolejkę w platformie Azure, więc na co czekamy? Z chęcią wykorzystamy popularne przetwarzanie bezserwerowe w naszej aplikacji! Zobaczmy kod adaptera wejściowego, który korzysta z kolejki w magazynie chmury Azure.

Można łatwo zauważyć, że oba adaptery korzystają z tego samego przypadku użycia. Tak zaprojektowany system znacząco zwiększa swoją rozszerzalność oraz uniwersalność. Dodatkowo zapewnia nam bezpieczeństwo warstwy biznesowej aplikacji, gdyż pozwala na ponowne użycie odpowiednich scenariuszy (przygotowanych oraz przetestowanych) dla różnych odbiorców.

Podsumowanie

Za nami solidna dawka wiedzy! Poznaliśmy podstawy teoretyczne, przejrzeliśmy wysokopoziomowy projekt systemu, jak i zgłębiliśmy tajniki implementacji na prostym przykładzie. Powyższe fragmenty kodu nie stanowią pełnej implementacji, jednak są świetnym sposobem na rozpoczęcie własnej przygody z wytwarzaniem opisywanego projektu.

Na sam koniec powtórzę główne cechy, które sprawiają, że architektura heksagonalna jest świetnym wyborem dla nietrywialnych aplikacji biznesowych. Po pierwsze – świetnie separuje aspekty biznesowe od technicznych. Po drugie – projektowanie prostych adapterów pozwala na podejmowanie pewnych decyzji na późniejszym etapie projektu. Na koniec, po trzecie – wymienność adapterów ułatwia testowanie, jak i eksperymentowanie.

Mam nadzieję, że dotrwaliście razem ze mną do końca, oraz że powyższy artykuł przypadł Wam do gustu. Liczę, że mogliście się trochę z niego nauczyć. Jeśli tak jest, to może chcecie sprawdzić wpis na temat integrowania z zewnętrznym systemem za pomocą testów? Niezależnie od wszystkiego – bardzo Wam dziękuję za uwagę! Do usłyszenia w kolejnym wpisie w Cesarstwie-Dev!


3 Komentarze

Jacek · 2020-10-03 o 08:49

Hej! Ciekawy post. Zainteresowało mnie w nim internal repozytorium. Skoro nic nie ma do niego dostepu z zewnatrz to w jaki sposob rejestrujesz go w kontenerze wstrzykiwania zależności? Implementujesz extension method dla IServiceCollection? W ten sposob zamykasz uzytecznosc biblioteki w aplikacjach w starym. Net frameworku

    Cesarz · 2020-10-03 o 10:54

    Cześć! Dzięki za trafny komentarz! Masz rację z implementacją extension method. Jestem przyzwyczajony do pisania aplikacji pod .NET Core, stąd takie podejście. W celu uzyskania większej reużywalności bibliotek rzeczywiście można zmienić kwantyfikator dostępu do repozytorium. Drugą opcją będzie stworzenie w warstwie środkowej interfejsu nakładającego abstrakcję nad warstwą rejestracji (np. IBookRepositoryProvider). Podejście z publicznym i internalowym poziomem dostępu ma swoje wady i zalety. Kwestia podjęcia decyzji i konsekwencji.
    Pozdrawiam!

dotnetomaniak.pl · 2020-10-01 o 09:11

Architektura heksagonalna w C#

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 *