Podczas tworzenia systemów informatycznych pojawiają się pytania dotyczące wyboru architektury i organizacji kodu. W dzisiejszym artykule chciałbym bliżej przedstawić architekturę portów i adapterów (ports & adapters) w praktyce oraz rozwiać wszelkie wątpliwości związane z tym wzorcem. Jakie są największe zalety architektury tupu porty i adaptery? Czy sprawdzi się ona w każdym przypadku?
Wzorzec porty i adaptery (ang. ports & adapters) określany również jako architektura hexagonalna (ang. hexagonal architecture) to nic innego jak wzorzec narzucający sposób budowy i organizacji kodu aplikacji. Głównym założeniem tego wzorca jest tworzenie kodu w taki sposób, aby maksymalnie odseparować implementację logiki biznesowej od wszelkich zależności zewnętrznych, takich jak frameworki, bazy danych, usługi zewnętrzne itp. Dzięki temu zyskujemy większą kontrolę nad kodem i niezależność od zewnętrznych bibliotek oraz wprowadzanych w nich zmian. Przyjrzyjmy się bliżej wzorcowi:
Wzorzec portów i adapterów dzieli strukturę kodu na dwa obszary:
Obszar wewnętrzny skupia się na rozwiązaniu i implementacji głównego problemu (use case). Obszar ten nie ma żadnych odniesień do frameworków, baz danych i innych usług lub bibliotek zewnętrznych, ani nie czerpie z nich. Obszar zewnętrzny z kolei zawiera implementację portów w postaci adapterów oraz klasy związane z frameworkami, jak na przykład Spring, Hibernate itp. Komunikacja pomiędzy obszarem wewnętrznym i zewnętrznym realizowana jest za pomocą portów i ich implementacji, czyli adapterów (stąd też nazwa wzorca).
Kilka podstawowych założeń architektury typu porty i adaptery:
Głównym celem przykładu jest zaprezentowanie implementacji klas i organizacja pakietów w oparciu o idee architektury typu ports & adapters. Za przykład niech posłuży obszar domeny odpowiedzialny za rejestrację nowych użytkowników w systemie. Zaimplementowane zostały tutaj dwa przypadki użycia:
To główny pakiet domeny, w obrębie której będziemy się poruszali. Czyli domeny użytkowników. Pakiet ten zawiera dwa podpakiety: crud oraz registration
W pakiecie tym została zamknięta logika związana z prostymi operacjami pobierania danych. Ponadto może on zawierać inne operacje, które nie posiadają złożonej logiki biznesowej i służą wyłącznie prostym operacjom typu CRUD (od: create, read, update and delete).
Ten pakiet zawiera wszystkie klasy związane z logiką rejestracji nowych użytkowników w aplikacji (implementacja i obsługa wspomnianych wcześniej przypadków użycia).
Pakiet zawiera klasy z główną implementację logiki biznesowej oraz definicje portów (interfejsy Java), za pośrednictwem których odbywa się komunikacja z innymi komponentami systemu. Pakiet core.user.registration.domain zawiera kod vanilla Java, tzn. kod, który nie jest zależny od frameworków i innych bibliotek zewnętrznych. Jedynym punktem dostępu do zaimplementowanej logiki w tym pakiecie jest klasa fasady. Co ważne: to, jakich bibliotek będziemy używali (np.: commons-lang, Dozer, Orika) wewnątrz pakietu, zależy od zespołu i ogólnych ustaleń w projekcie.
Pakiet ten zawiera implementację adapterów, które wykorzystywane są za pośrednictwem zdefiniowanych w kodzie domeny portów. W pakiecie zamknięta jest implementacja i konfiguracja komponentów korzystających z funkcji i metod frameworków. Często używane są tutaj biblioteki innych dostawców rozwiązań, które są odpowiedzialne np. za generowanie plików PDF, wysyłanie maili czy komunikację z systemami zewnętrznymi, takimi jak Kafka, SOAP itp.
W pakiecie tym powinna znaleźć się konfiguracja związana z tworzeniem i obsługą komponentów wykorzystywanego frameworka.
Pakiet ten zawiera klasy kontrolerów i klasy typu DTO, za pośrednictwem których inne systemy czy moduły mogą korzystać z funkcji naszej domeny. Klasy DTO opisują struktury danych wejściowych i wyjściowych dla REST API.
Pakiet klas związanych z warstwą persystencji, czyli klasy odpowiedzialne za pobieranie i zapisywanie danych w bazie. Są to definicje modelu danych (ORM), klasy DAO. Pakiet ten zawiera m.in. implementację klasy adaptera dostępu do danych. W bardziej rozbudowanych przypadkach możemy mieć więcej klas modelu lub odwoływać się do klas modelu (ORM) z innych pakietów.
Wyżej wymienione porty to publiczne interfejsy klas Java, których implementacja odbywa się na poziomie pakietów infrastruktury.
UserRegistrationFacade – fasada domeny. Klasa, za pośrednictwem której mamy dostęp do metod (funkcji) domeny. W przedstawionym przykładzie klasa fasady pełni funkcję wywołującą dla klasy UserRegistration, czyli pełni dla niej funkcję wrappera. W bardziej złożonych przypadkach możemy posiadać więcej klas zawierających logikę biznesową. Wówczas fasada stanowi punkt dostępu do wywołań wszystkich tych funkcji domeny. Czasami może się zdarzyć, że będziemy potrzebowali dołożyć na tym etapie określone walidacje związane z frameworkami. Może być to podyktowane tym, że niektóre inne obszary domenowe będą potrzebowały wykorzystać logikę z innej domeny. Można wówczas na poziomie fasady korzystać z funkcji walidacji. Oczywiście wszystko podobnie jak we wspomnianych wcześniej przypadkach zależy od ogólnych ustaleń i założeń projektowych w zespole.
Implementacja adapterów:
W przykładowej implementacji pakietu domain widać dużą liczbę klas – jest to jeden z atrybutów tego wzorca. Dzięki temu uzyskujemy hermetyczność implementacji logiki biznesowej i struktur z nią związanych. Punktem dostępu do funkcji domeny jest klasa fasady. Z założenia klasy pakietu domain nie powinny używać klas frameworków, stąd też większa liczba klas POJO (plain old Java object) związanych z danymi. W wypadku złożonych przypadków użycia można tworzyć większą liczbę klas z implementacją logiki biznesowej. W przykładzie można np. wydzielić aktywację konta do osobnej klasy.
Przeczytaj także: Test-Driven Development na co dzień
Dlaczego musimy ograniczać dostęp do niektórych klas na poziomie pakietu?
Za pomocą modyfikatorów dostępu do klas i metod ukrywamy szczegóły implementacji – czyli stosujemy tzw. hermetyzację klas. Zapewniamy tym samym kontrolę nad dostępem, stanem i zachowaniem obiektu. W naszym przykładzie w pakiecie domain dostęp publiczny posiadają: klasa fasady oraz klasy reprezentujące dane wejściowe i wyjściowe. Cała implementacja logiki jest zamknięta na poziomie pakietu i dostęp do operacji jest możliwy wyłącznie za pośrednictwem metod zdefiniowanych w klasie fasady.
Co z bibliotekami typu Lombok, JSR 303 (walidacja) lub loggerami w pakiecie domain?
Zgodnie z główną ideą architektury hexagonalnej nie powinniśmy używać bibliotek zewnętrznych. Praktyka dowodzi jednak czegoś innego. To, jakich bibliotek użyjemy wewnątrz domeny, zależy od decyzji zespołu, gdyż każda osoba w zespole ma inne doświadczenia i pomysły. Warto jednak zastosować pewne ograniczenia co do tych narzędzi. W projektach, w których brałem udział, ograniczaliśmy się głównie do bibliotek typu java commons, biblioteki mapperów oraz walidacji.
Porty i adaptery – gdzie zakładać transakcje?
Jednym z ważniejszych elementów implementacji operacji jest ich transakcyjność.
Często wymagana operacja będzie potrzebowała wykorzystać funkcję z innej domeny kodu lub będziemy chcieli wywołać funkcję domeny w transakcji. Wówczas musimy zapewnić transakcyjność dla wywołań tych funkcji. Dla takiego przypadku można zbudować dedykowany serwis, który będzie zawierał klasy fasad domenowych.
W przykładzie serwis został zaimplementowany w klasie UserRegistrationService.
Dlaczego nie powinniśmy zakładać transakcji w klasie fasady?
Brak transakcji w klasie fasady wynika z założenia, że kod w pakiecie domain ma być wolny od wszelkich frameworków.
Czy można używać DDD razem ze wzorcem ports & adapters?
DDD (Domain-Driven Design) skupia się na domenie i zawartej w niej logice biznesowej. Nie ma nic wspólnego z frameworkami i transakcjami. W przykładzie część związana z DDD została zaimplementowana w pakiecie domain. Wzorzec ports & adapters pomaga nam uporządkować kod i odseparować dostęp do zaimplementowanej logiki. Można stwierdzić, że wzorzec ten bardziej odpowiada za warstwę aplikacyjną rozwiązania, ponieważ odpowiada m.in. za dostarczenie mechanizmów konfiguracji, transakcji czy persystencji. Podejście DDD oraz wzorzec ports & adapters współgrają ze sobą w strukturach kodu. DDD rozwiązuje problem logiczny, a P&A zapewnia dostęp do technicznych mechanizmów i bibliotek.
Czy zawsze powinniśmy stosować wzorzec ports & adapters?
Jeżeli zastanawiamy się, kiedy stosować architekturę hexagonalną, decydujący będzie aspekt logiki biznesowej. Wzorzec ports & adapters idealnie sprawdzi się w przypadku rozbudowanej logiki biznesowej, gdy chcemy skupić się na rozwiązaniu problemów, które zostały tam postawione. Kod jest wolny od wszelkich frameworków, dzięki czemu łatwo się nim zarządza i testuje. jest to, że testy uruchamia się szybko i pisze łatwo. Piszemy mniej testów integracyjnych – dużą część logiki pokrywają testy jednostkowe. W testach integracyjnych weryfikujemy krytyczne ścieżki.
Podejście to narzuca nam tworzenie większej liczby klas w celu odseparowania się od reszty kodu spoza pakietu, co na pierwszy rzut oka wydaje się nadmiarowe. Dzięki temu jednak łatwiej jest wydzielić logikę do osobnych niezależnych modułów (w architekturach mikroserwisowych często pojawia się potrzeba wydzielenia logiki do osobnej usługi).
W przypadku aplikacji, które posiadają mało logiki biznesowej lub wręcz jej nie ma (aplikacje typu CRUD), lepszym podejściem jest budowa struktury kodu w formie wielowarstwowej (Controller – Service – Repository). W przykładzie został wydzielony pakiet CRUD, w którym zaimplementowana została prosta operacja pobrania listy użytkowników.
Mam nadzieję, że udało mi się przedstawić główne idee budowy kodu w architekturze portów i adapterów oraz odpowiedzieć na część pytań, które mogą pojawić się podczas implementacji. Oczywiście w każdym projekcie może wyglądać to inaczej. Wszystko zależy od Was. To zespół tworzy oprogramowanie – organizacja i struktura kodu powinna być czytelna dla wszystkich i umożliwiać wygodną pracę. Tak naprawdę to, jak zaimplementowany zostanie wzorzec, zależy więc od zespołu i jego decyzji.
Zobacz więcej:
Chcesz dowiedzieć się więcej o naszych usługach? Napisz do nas – odpowiemy na każdą wiadomość.
Dodaj komentarz