Kilka pytań o Clean/Hexagonal Architecture/Ports Adapters

0

Próbuję aktualnie połączyć wiedzę w praktyce, którą zdobyłem z książki Clean Architecture, z różnych artykułów o Hexagonal Architecture/Ports Adapters oraz o stosowaniu podejścia package-scope/published, które przedstawił Kuba Nabradalik w swojej prezentacji:
Kod znajduje się tu: https://github.com/jakubnabrdalik/hentai

Mam jednak kilka pytań, zwłaszcza, że widząc rózne przykłady w internecie, każdy podchodzi do tego inaczej.

  1. Jaka powinna być struktura i nazewnictwo pakietów, które przechowują adaptery wchodzące/wychodzące (incoming/outgoing adapters), domenę oraz porty?
    Czy application/adapters na aprzechowywanie adapterów wchodzących, domain na domenę oraz infrastructure dla adapterów wychodzących to dobre podejście?

W jednym podejściu widziałem, że adaptery wchodzące są przechowywane a pakiecie application, natomiast wychodzące w pakiecie infrastructure:

└── com
    └── github
        └── shop
            ├── application
            │   └── controller
            │       └── OrderController.java
            ├── domain
            │   ├── dto
            │   ├── entity
            │   ├── OrderFacade.java
            │   └── ports
            │       ├── incoming
            │       └── outgoing
            └── infrastructure
                ├── console
                └── repository

Z kolei np w przykładzie Jakuba Nabradalika z linku, występuje tylko domain, w którym nawet nie ma pakietu ports, więc w pakiecie domain pomieszane ze sobą incoming/outgoing ports, a repozytoria np. OrderRepository, InMemoryOrderRespitory również są w pakiecie domain, ale nie są pogrupowane oraz infrastructure, natomiast brakuje adapters.

Które z podejść jest Waszym zdaniem najbardziej polecane i czy powinnśmy dzielić porty również na podpakiety incoming/outgoing?

  1. Jaka powinna być rola Fasady? Czy fasada powinna tylko i wyłącznie komunikować się z incoming/outgoing adapters np. być wywoływana z kontrolera np. RestController, czy może również zawierać logikę biznesową, lub nie powinna i logika biznesowa lub najróżniejsze operacje Domeny powinny znajdować się w klasach np OrderService, które odpala Fasada lub tez w obiektach domenowych (Entity Object) w pakiecie domain?

  2. Czy w przypadku zastosowania monolitu i obecności większej liczby komponentów, powinno się stosować package-by-feature, żeby odzielić od siebie rózne funkcjonalności biznesowe na przykładzie sklepu: komponents Complaints do reklamacji, Orders do zamówień, Shipping do wysyłek, Invoice do faktur:

└── com
    └── github
        └── shop
            ├── complaints
            │   ├── application
            │   │   └── controller
            │   │       └── ComplaintsController.java
            │   ├── domain
            │   │   ├── ComplaintsFacade.java
            │   │   ├── dto
            │   │   ├── entity
            │   │   └── ports
            │   │       ├── incoming
            │   │       └── outgoing
            │   └── infrastructure
            │       └── repository
            ├── invoice
            │   ├── application
            │   │   └── controller
            │   │       └── InvoiceController.java
            │   ├── domain
            │   │   ├── dto
            │   │   ├── entity
            │   │   ├── InvoiceFacade.java
            │   │   └── ports
            │   │       ├── incoming
            │   │       └── outgoing
            │   └── infrastructure
            │       └── repository
            ├── order
            │   ├── application
            │   │   └── controller
            │   │       └── OrderController.java
            │   ├── domain
            │   │   ├── dto
            │   │   ├── entity
            │   │   ├── OrderFacade.java
            │   │   └── ports
            │   │       ├── incoming
            │   │       └── outgoing
            │   └── infrastructure
            │       ├── console
            │       └── repository
            └── shipping
                ├── application
                │   └── controller
                │       └── ShippingController.java
                ├── domain
                │   ├── dto
                │   ├── entity
                │   ├── ports
                │   │   ├── incoming
                │   │   └── outgoing
                │   └── ShippingFacade.java
                └── infrastructure
                    └── repository
  1. Czy taka struktura pakietów jak ma również sens w przypadku kiedy system jest podzielony ma serwisy/mikroserwisy, gdzie każdy mikroserwis jest w osobnym repozytorium. ma osobną bazę danych oraz ogólnie stanowi osobny Bounded Context/Subdomain w kontekście DDD, czy może zwykła tradycjny podział typu controller/service/domain spokojnie wystarczy, zwłaszcza kiedy mikroserwisy są osobnymi projektami i znajdują się w innych repozytoriach?

Z góry dzięki ;)

3

Jego przykłady są pisane w javie i dlatego muszą obchodzić javowe problemy z widocznością. Jeśli możesz sugerowałbym kotlina, ponieważ podział na moduły sprawdza się tutaj dużo bardziej i znika całkowicie problem z podziałem na pakiety, ponieważ modułom musisz podać jawnie zależności.

  1. Lepiej jak fasada nie zawiera logiki. jedynie wywołania odpowiednich obiektów dalej. Ułatwia to jakiś sensowny podział na odpowiedzialności.
  2. Tak
  3. Generalnie przestaw się z myślenia warstwami na myślenie takie, że jest domena i rzeczy naokoło niej (jeśli chcesz robić hexagona). To jest prawie to samo, ale nie ma aż tak sztywnego nazewnictwa. Jeśli masz kod podzielony na mikroserwisy, to znowu zależy od tego jak są duże. Jeśli małe, to czasem szkoda zabawy.
1

Co do punktu 3.

Czy taka struktura nie powoduje, że jednak klasy musza byc publiczne? Bo w fasadzie jednak musimy skorzystać z klas z innych pakietów (pod-pakietów)...?

3

Zrób tam jak człowiek maven/gradle multi-module project i problemy same znikną, bo będziesz nagle miał podział 2-stopniowy, nie tylko pakiety (które w ogóle nie dają żadnej sensownej separacji) ale też moduły które separują porządnie bo generują osobne artefakty.

I nagle to application, czy domain stają się osobnymi modułami.

0

Dzięki za odpowiedzi!

Generalnie przestaw się z myślenia warstwami na myślenie takie, że jest domena i rzeczy naokoło niej (jeśli chcesz robić hexagona). To jest prawie to samo, ale nie ma aż tak sztywnego nazewnictwa. Jeśli masz kod podzielony na mikroserwisy, to znowu zależy od tego jak są duże. Jeśli małe, to czasem szkoda zabawy.

Przestawiłem już się na myślenie o Domenie, tylko chciałem własnie wyjaśnić sobie kilka szczegółów, i pomogłeś, dzięki :D

Zrób tam jak człowiek maven/gradle multi-module project i problemy same znikną, bo będziesz nagle miał podział 2-stopniowy, nie tylko pakiety (które w ogóle nie dają żadnej sensownej separacji) ale też > moduły które separują porządnie bo generują osobne artefakty.
I nagle to application, czy domain stają się osobnymi modułami.

Jasne, tak zrobiłbym docelowo. Chodzi Ci o to, żeby wydzielić moduły także według funkcjonalności biznesowej? W tym przykładzie mielibyśmy 4 moduły Maven/Gradle: shop-complaints, shop-orders, shop-invoice, shop-shipping, a w środku nich już oczywiście pakiety domain, application, infrastructure itd? Czy chodzi o to, żeby jeszcze oprócz tych 4 ww modułów tzn complaints, orders, invoice, shipping, wydzielić więcej modułów tzn modułem Maven/Gradle stałyby się także pakiety application/domain/infrastructure?

2

Czy chodzi o to, żeby jeszcze oprócz tych 4 ww modułów tzn complaints, orders, invoice, shipping, wydzielić więcej modułów tzn modułem Maven/Gradle stałyby się także pakiety application/domain/infrastructure?

This. Moim zdaniem to o wiele lepsze rozwiązanie, bo dzięki temu zupełnie rozpinasz domenę od decyzji frameworkowych/technologicznych. Masz sobie moduł application który np. ma jakieśtam powiązania z frameoworkiem webowym którego używasz, ale to nie "wycieka" nigdzie dalej. Moduły "domenowe" w ogóle nic na ten temat nie wiedzą i są zupełnie niezależne. Tak samo jakieś DTO też wyrzucam do osobnego modułu client, razem z programowym klientem do danego serwisu. Dzięki temu inny serwis który chce korzystać z mojego, dodaje sobie zależność na ten artefakt client i voila, ma klienta i wszystkie potrzebne DTO.
Tak samo "repozytoria" też lecą do osobnego modułu, tak żeby domena nie była związana z modelem "bazodanowym". Domena powinna polegać tylko na jakimś interfejsie repozytorium i niczym więcej.

0

This. Moim zdaniem to o wiele lepsze rozwiązanie, bo dzięki temu zupełnie rozpinasz domenę od decyzji frameworkowych/technologicznych. Masz sobie moduł application który np. ma jakieśtam powiązania z frameoworkiem webowym którego używasz, ale to nie "wycieka" nigdzie dalej. Moduły "domenowe" w ogóle nic na ten temat nie wiedzą i są zupełnie niezależne. Tak samo jakieś DTO też wyrzucam do osobnego modułu client, razem z programowym klientem do danego serwisu. Dzięki temu inny serwis który chce korzystać z mojego, dodaje sobie zależność na ten artefakt client i voila, ma klienta i wszystkie potrzebne DTO.
Tak samo "repozytoria" też lecą do osobnego modułu, tak żeby domena nie była związana z modelem "bazodanowym". Domena powinna polegać tylko na jakimś interfejsie repozytorium i niczym więcej.

Mega ciekawe i eleganckie rozwiązanie, dzięki! Ja niestety poza używanie pojedynczych modułów Mavenowych nie wyszedłem, bo jakoś nie było okazji z tym popracować.., nawet nie wiedziałem, ze można zagnieżdżać moduły w sobie na kilka poziomów drzewa :D

A w pojedynczym mikroserwisie, który ma osobne repo, też poszedłbyś za tym rozwiązaniem?

0

A jak już mowa o modułach. Co Panowie powiecie o java 9 feature: project Jigsaw?

0

Ostatnio spotkałem się z takim "problemem":
Duży projekt, pełno architektów, architektura mikroserwisów.
Piszesz sobie test. Albo po prostu jakąś funkcjonalność. Tworzysz jakąś dajmy na to zmienną która poweidzmy że może być użyta w wielu mikroserwisach jako stała. Więc dostajesz w code review info, "ej wiesz co, wrzuć to do common-test". I tutaj zaczyna się cała historia.

  1. Dodajesz property do common-test
  2. Common-test wchodzi w skład common-library
  3. Common-library wchodzi w skład common-pom
  4. Common-pom masz w projekcie gdzie wrzucasz zmiane.

Musze podnosić wersje 4 projektów/bibliotek żeby dodać jeb*** globalne property. Chyba nie o to chodzi w architekturze mikroserwisowej? Jak dla mnie cała idea common-cokolwiek jest chora. Wszystko powinno zamykać się w obrębie jednego mikroserwisu. Koniec kropka.
Co o tym myślicie?

2

Genialny plan i potem masz 17 razy skopiowany ten sam kod? I jak trzeba zrobić poprawkę, to robisz ją w tych 17 miejscach? :)
Twój przykład jest mocno z dupy, bo w praktyce commons to pewnie jeden projekt, może z kilkoma modułami, więc realnie wygląda to tak:

  • robię zmianę
  • pushuje
  • CI buduje nową wersję commonsów i wrzuca do artifact repository
  • zmieniam wersje zależności w projekcie w którym chciałem z tego korzystać

Nawet jak żyjesz w roku 2000 i nie masz CI, to ewentualnie:

  • robię zmianę
  • podbijam wersje commons
  • robie maven delpoy
  • zmieniam wersje zależności w projekcie w którym chciałem z tego korzystać

Nadal nie jest to specjalnie skomplikowane i na pewno sensowniejsze niż kod skopiowany X razy.

5

Nie do końca rozumiem. W takim wypadku za określone "entity" musimy mieć 3 oddzielne klasy. Do modułu domain wrzucamy plain java objects dla modelu domenowego i jakiś interfejs do repozytorium. Teraz musimy w module Infrastructure zaimplementować interfejs z domain, zrobić jakiś adapter, zmapować na entity (w końcu domain jest wolne od frameworków). A application layer podczas czytania mapuje wszystko do jakiegoś DTO. Nie jest to trochę over-engineering? Ciekawi mnie sama idea wielomodułowego projektu i jak można to zastosować na przykładzie ŚREDNIEGO systemu.

@Korges i tak w każdym normalnym systemie musisz mieć takie 3 osobne modele, chyba ze piszesz CRUDa i one się mapują 1:1. W normalnym życiu, jak masz serwis który jednak coś robi a nie tylko przepycha json->sql i odwrotnie, tak nie jest :) Model bazodanowy często nijak sie ma do modelu domenowego (który jest o wiele bardziej rozbudowany, często dociąga sobie różne dane z innych serwisów itd.) i nijak się te dwa modele mają do DTO które wchodzą/wychodzą z systemu (bo te zwykle są bardzo mocno "okrojone")

Prosty przykład z życia wzięty:
Mamy serwis który pozwala ściągać z naszego archiwum zdjęcia astronomiczne wraz z powiązanymi plikami (kalibracje itd).

  1. Wejściowe DTO to lista ID plików które użytkownik sobie wyszukał (za pomocą innego systemu).
  2. Model domenowy, na podstawie tych ID dociąga ID wszystkich powiązanych plików, informacje o ich kategoriach, rozmiarze, prawach dostępu... (w praktyce model jest jeszcze bardziej rozbudowany, ale uznajmy że tyle, zeby nie zaciemniać obrazu).
  3. Jedno wyjściowe DTO to lista kategorii (nie pojedynczych plików, bo tych mogą być tysiące) ze zbiorczym podsumowaniem liczby plików i rozmiarów, gdzie user może w UI może sobie powybierać kategorie, które faktycznie chce pościągać. Inne wyjściowe DTO to np. lista bezpośrednich linków do wszystkich plików z wybranych przez użytkownika kategorii, tak żeby mógł ja wrzucić do jakiegoś download managera.
  4. Model "bazodanowy" trzyma informacje na temat tych wejściowych ID podanych przez użytkownika plus kategorie (i inne rzeczy) które user sobie powybierał, bo tyle potrzebujemy żeby załadować do pamięci model domenowy danego requestu użytkownika.

Zauważ, że generalnie te modele nijak się do siebie mają, a przecież ten serwis nie jest nawet średni tylko mały :) Taka sytuacja gdzie wszystko da się upchnąć w jednym modelu występuje tylko w tutorialach i CRUDach.

0

@Korges: wydaje mi się, że na rozwiązanie tych problemów jest kwestia doboru protokołu, który obsługuje microS. np. Redis, Kafka, Rabbit, ZeroMQ (świetny MQ niedoceniany). Chodzi mi dokladnie o to:
https://grapeup.com/blog/reactive-service-to-service-communication-with-rsocket-introduction/

1 użytkowników online, w tym zalogowanych: 0, gości: 1