Przejdź do treści
Tło

Blog

Architektura porty i adaptery w języku Java

Ikona

Od wielu lat przyjętym standardem w świecie Javy jest podział backendu aplikacji webowych na 3 główne warstwy: 1) kontrolery (endpointy) przyjmujące żądania od interfejsu użytkownika, 2) warstwa logiki biznesowej zbudowana z encji i serwisów na nich operujących, 3) warstwa persystencji, która udostępnia odczyt i zapis owych encji w bazie danych lub warstwa z zewnętrznymi usługami. Encje, czyli obiekty domenowe reprezentujące rzeczowniki zebrane podczas analizy wymagań mają swoje pola, gettery i settery (tzw. anemiczny model domenowy), których pola i odzwierciedlają kolumny tabeli w bazie danych. Endpointy korzystają i zależą od warstwy logiki biznesowej, a logika biznesowa korzysta i zależy od warstwy persystencji. W skrócie: warstwa wyższa zależy od warstwy niższej i jest skazana na korzystanie z tego co ona umożliwia. O ile w przypadku obiektów, których logika sprowadza się do tworzenia, edycji, usuwania i wyszukiwania (CRUDy – Create, Read, Update, Delete) to w przypadku, gdy pojawiają się pewne reguły biznesowe taka architektura staje się mało wygodna z kilku powodów.

Po pierwsze w serwisach (warstwa logiki biznesowej) zaczyna pojawiać się coraz więcej ifów przez co coraz łatwiej pominąć w którejś z metod jakiś warunek. Po drugie testowanie wymaga co najmniej bazy danych w pamięci, co wydłuża proces testowania (jeżeli zdecydujemy się na z jakichś względów na użycie typu lub funkcji dostępnej tylko w konkretnej bazie danych to szybkie testowanie bez prawdziwej bazy danych staje się niemożliwe). Po drugie wymusza zaczęcie prac od przygotowania warstwy persystencji – musimy wybrać i skonfigurować narzędzie, z którego chcemy korzystać, ponieważ to od niego zależy jakie metody udostępnimy warstwie logiki biznesowej. Po trzecie, w przypadku korzystania z zewnętrznych usług (np. kolejka wiadomości, zewnętrzne usługi, biblioteka do autoryzacji) do warstwy logiki biznesowej przeciekają szczegóły tych warstw, a co za tym idzie, zmiany w warstwach niższych będą wymuszać zmiany w kodzie z logiką biznesową – co jest złamaniem zasady pojedynczej odpowiedzialności (Single Responsibility Principle), która mówi, że nie powinno być więcej niż jednego powodu do zmiany klasy.

Ratunek

Rozwiązaniem powyższych problemów byłoby odwrócenie zależności – to nie warstwa logika biznesowa powinna zależeć od warstw niższych, ale to warstwa niższa powinna dostarczać to co potrzebuje logika biznesowa.

Porty i adaptery

Architektura, która implementuje taką odwróconą zależność, nazywana jest porty i adaptery (lub inaczej architektura heksagonalna – ze względu na to, w jaki sposób jest przedstawiana na diagramach). Składa się z jądra, w którym zawarta jest logika biznesowa, portów i ich adapterów. Porty – są to interfejsy, których właścicielem jest jądro i dzielą się one na 2 typy: porty wejściowy i wyjściowe (lub wg innego nazewnictwa primary i secondary). Porty wejściowej są to interfejsy, z których świat zewnętrzny może korzystać, aby wykonać dostępne operacje. Porty wyjściowe są interfejsami używanymi przez logikę biznesową. Zapewniać mogą dostęp do bazy danych, zewnętrznych systemów lub innych modułów tego samego systemu. Adaptery wejściowe korzystają z portów wejściowych (są to np. endpointy REST). Adaptery wyjściowe są implementacją interfejsów z portów wyjściowych (np. dostęp do bazy danych czy komunikacja z zewnętrznym systemem). Właśnie tutaj kryje się cała magia tej architektury – adaptery wyjściowe izolują naszą logikę biznesową od zewnętrznych zależności. Możemy rozwijać i stale testować najważniejsze funkcje systemu bez podejmowania innych ważnych decyzji architektonicznych (np. wybór silnika bazy danych czy biblioteki do komunikacji z bazą danych).

Inna struktura pakietów

Powyższy przykład jest jedną z wielu opcji struktury pakietów. W przypadku małych domen taka liczba pakietów może być przerostem formy nad treścią. Wtedy można ograniczyć się na przykład do pakietów “api”, “domain”, “infrastructure”. Z kolej przy rozbudowanych modelach możemy chcieć pogrupować porty w podpakietach “primary” i “secondary”. Można zacząć od wersji prostszej i, gdy uznamy, że kod się znacząco rozrósł, przeprowadzić refaktoryzację do większej liczby podpakietów. Najważniejsze jest ustalić w ramach całej organizacji, zespołu lub projektu wspólny standard.