Architektura hexagonalna a encje z wieloma zależnościami

0

W ramach samorozwoju postanowiłem napisać swój system do zarządzania projektami (jak Jira, Trello, Redmine, itp). Przy okazji chciałem stosować się do wytycznych z tego filmiku

O ile dobrze rozumiem, u podstawy każdego takiego modułu leżała by jakaś encja biznesowa i zarazem encja w rozumieniu bazy danych.
We wspomnianym wyżej projekcie mógłbym wydzielić moduły jak:
-project
-sprint
-task
Każdy z nich udostępniałby podstawowe CRUDowe operację, a poza tym specyficzne dla danego poziomu rzeczy jak. wykres spalania w danym sprincie, wyświetlanie release mapy danego projektu, itp. Powstaje jednak techniczny problem - klasy wewnątrz danego modułu powinny mieć package scope i dane pakiety powinny rozmawiać przez fasady, jednocześnie, po stronie bazy te encję mają relacje, taki task jest podpięty do jakiegoś sprintu. Po stronie springa, którego chcę wykorzystać, wymusza to stworzenie referencji do obiektu Sprint wewnątrz obiektu Task, co jest niemożliwe jeżeli te klasy powinny być zamknięte w innych innych package scopeach.

Jak sobie z tym poradzić?
Jedyną opcję jaką widzę jest dodatkowy poziom abstrakcji między bazą danych a logiką biznesowa, tzn. osobny moduł persistance, trzymający obiekty tworzące strukturę bazy danych ze wszystkimi relacjami i springowymi adnotacjami i udostępniający CRUDowe operację, a komponenty biznesowe wykorzystywałyby go. Wymaga to jednak powstania dodatkowych mapowań, z obiektów domenowych/biznesowych na klasy Entity.

Chyba, że jest jakaś inna opcja której nie dostrzegam, bądź źle rozumiem całe podejście hexagonalne?

0

Pierwsza i najważniejsza rzecz:
Encja biznesowa != encja bazodanowa

Druga ważna rzecz:
Nie brudź logiki biznesowej frameworkami (zostaw na razie springa z boku, posłuży on dopier do złożenia całości do kupy, jeśli w ogole go potrzebujesz)

Trzecia rzecz:
Architektura heksagonalna to tak na prawdę architektura warstwa, z tym, że każdy moduł ma swoje warstwy.

Powinieneś podzielić projekt na moduły (może z tego wyjdzie Ci tylko jeden moduł), które mają jakąś pewną odpowiedzialność na poziomie całego projektu. Jako przykład z Twoimi danymi:
moduł project- miałbyś w nim wszystkie rzeczy związane z projektem (projekt, taski, ewentualnie sprinty). Przykładowe metody z fasady to createTask, setTaskStatus
moduł sprint - służył by do zarządzania sprintami, otwieranie, zamykanie generowanie raportów

Powiązania między danymi z różnych modułów:
U mnie każdy "zasób" ma swoje UUID i pomiędzy modułami zapisuje tylko uuid "zasobu" do którego się odnoszę.

Komunikacja między modułami, warstwami:
Wszystkie moduły i warstwy rozmawiają ze sobą po przed DTO. Po co? Niektóre obliczenia łatwiej jest przechowywać w inny sposób niż są czytelne dla końcowego użytkownika (np. wykorzystują jakieś skomplikowane struktury danych). Tak samo w momencie pisania logiki biznesowej nie powinno Cie obchodzić jak to potem będzie gdzieś zapisywane. Dopiero w momencie jak przekażesz jakiś obiekt do zapisania (w formie DTO) warstwie persistance, to ta warstwa przerabia sobie ten model na jakąś odpierdną strukturę do zapisu do bazy danych (np dodaje jakieś tabele itp).

Trochę może być chaotycznie napisane, ale to dość duży temat ;)

0

Nie brudź logiki biznesowej frameworkami (zostaw na razie springa z boku, posłuży on dopier do złożenia całości do kupy, jeśli w ogole go potrzebujesz)

Fasada, serwisy, utilsy, klasy pomocnicze - tutaj nie jest specjalnie zabrudzone frameworkiem. Wstrzykuje jedynie odpowiednie repozytorium, ale tutaj przejście ze springowego CrudRepository na jakieś InMemoryDbRepository to kwestia modyfikacji 1 linijki, bo całe DI realizuję w jednej klasie

@Configuration
class TaskConfiguration {
    @Bean
    TaskFacade taskFacade(TaskRepository taskRepository) {
        TaskFacade taskFacade = new TaskFacade(taskRepository);
        return taskFacade;
    }
}

Zabrudzenie frameworkiem pojawia się w warstwie encji, no ale tutaj też pozbycie się go == usunięcie adnotacji.
Co do mojego pierwotnego problemu - możliwe że znalazłem rozwiązanie. Zakładałem błędnie, że to aplikacja będzie tworzyć bazę i wszelkie relacje. Obecnie robię zamiast tego to co zaproponowałeś, w encjach trzymam jedynie ID kluczy obcych

class Task {
    @Id
    @GeneratedValue
    @Column(name = "task_id")
    private Integer taskID;
    @Column
    private String title;
    @Column
    private String details;
    @Column(name = "user_id")
    private Integer userID;
}

A tworzenie tabeli z odpowiednimi relacjami robię z poziomu SQLa. Wymagać to będzie nieco więcej gimnastyki przy pisaniu kodu, ale innego rozwiązania tutaj nie widzę.

0

A potrzebujesz tych relacji na poziomie SQLa?
edit
Jeśli idziesz w mocną separację między modułami to możesz załóżyć, że każdy moduł operuje na osobnej bazie danych (nawet jeśli fizycznie jest to ta sama baza)

0

Ostatnio zastanawiałem się praktycznie nad tym samym, cała koncepcja z architekturą heksagonalną brzmi świetnie, tylko ciągle mam wrażenie, że jest łatwa do zastosowania tylko przy projektach, które nie mają zbyt wielu zależności między obiektami. Z mojej strony zaproponowałbym bardziej coś w takim kierunku.

Dla przykładu załóżmy, że pracujemy z dokumentami. Każdy dokument posiada:

  • **DocumentHeader **- nagłówek dokumentu, każdy dokument zawiera jeden.
  • **DocumentBody **- pozycja dokumentu, każdy dokument zawiera przynajmniej jedną.
  • **DocumentSummary **- podsumowanie dokumentu w danej stawce vat - przynajmniej jedno.

Do każdego z tych elementów masz jakiś tam serwis, w którym znajduje się logika do zapisu, walidacja, pobieranie danych. Później jest to udostępniane na światło dzienne przy pomocy fasady. Te fasady powinny zostać przetestowane szybkimi testami jednostkowymi - to tylko drobny element, odnosi się do jednej encji, tutaj nie potrzebujesz robić żadnych testów integracyjnych. (To miejsce tak jak wspomniałeś na skorzystanie z InMemoryDbRepository)

Wszystko fajnie, ale teraz chcemy zrobić korektę dokumentu - trzeba pobrać dane ze wszystkich serwisów, przetworzyć i zmodyfikować na podstawie danych wprowadzonych przez użytkownika. Tworzymy sobie teraz serwis w głównym pakiecie dla dokumentów - DocumentCorrectionService, w nim wołamy wszystkie potrzebne fasady oraz przetwarzamy otrzymane dane. Możemy dalej sobie rozbijać to na klocki, realizujące już jakąś konkretną funkcję, np. przeliczenie pozycji, czy co tam sobie wymyślimy. Kiedy już mamy napisane to co chcieliśmy, to udostępniamy to przez publiczną fasadę. Tego elementu już nie musimy testować jednostkowo, ponieważ wszystkie składowe mamy przetestowane na niższym poziome. Tutaj możemy śmiało napisać sobie do tej fasady test integracyjny, postawić sobie bazę danych w pamięci, dzięki temu sprawdzimy sobie spójność komponentów, sprawdzimy jak się zachowa system dla danej funkcji.

  • Documents - DocumentHeader - DocumentHeaderFacade
  • Documents - DocumentHeader - DocumentHeaderService
  • Documents - DocumentBody - DocumentBodyFacade
  • Documents - DocumentBody - DocumentBodyService
  • Documents - DocumentSummary - DocumentBodyFacade
  • Documents - DocumentSummary - DocumentBodyService
  • Documents - DocumentsFacade
  • Documents - DocumentCorrectionService

Co do jeszcze zależności pomiędzy obiektami, czyli np. relacja jeden do wiele - DocumentHeader z DocumentBody, to bardziej bym poszedł w rozszerzenie klasy bazowej DocumentHeader, która nie posiadałaby żadnych relacji, w kolejnej klasie. O ile musisz mieć taką relację, to wydaje się to jedyne sensowne rozwiązanie, bo nie spowoduje Ci dodatkowego narzutu na encje, kiedy nie będziesz tego potrzebował. (z lazy bywa różnie ;p) Chociaż może się okazać, że nawet do końca nie potrzebujesz tej relacji na obiekcie, bo przeliczyć jakieś pola możesz na etapie tworzenia samego zapytania w jpql i zwrócić sobie bezpośrednio DTO z obliczonymi polami. No chyba, że chcesz się bawić w kaskadowe zapisywanie, usuwanie etc., ale moim zdaniem funkcja nie warta zachodu. Powiązanie jeden do jeden, będzie jeszcze prostsze, bo tutaj wszystko możesz sobie zwrócić bezpośrednio w DTO, ewentualnie możesz zrobić to samo co pisałem wcześniej + ew. dochodzi jeszcze opcja z wykorzystaniem @Formula.

1

@Robertosen:
Tylko po co w tym wszystkim serwisy? Czy Dokument jako obiekt nie może realizować tych operacji? (Z perspektywy kogoś kto go używa będzie to jeden obiekt, implementacyjnie będzie to pewnie kilka)
Poza tym wszystkie operacje które bezpośrednio używają tego dokumentu powinny być w jednym pakiecie bo według czego innego chcesz dzielić pakiety?

0

@Robertosen: poczytaj sobie o aggregate root - w Twoim przypadku będzie nim klasa Document i tylko do niej będziesz potrzebował mieć repozytorium oraz fasadę. Miej na uwdze, że repozytorium oraz fasada powinny enkapsulować wysokopoziomowy dostęp do danych - nawet jeśli pod spodem będziesz miał piętnaście osobnych tabeli, repozytorium powinno być jedno - w innym wypadku będzie to najzwyklejsze table gateway.

0

@Robertosen: do tego co napisał @danek, to mam wrażenie, ze brakuje w niej dość istotnego elementu jakim jest Document (ten Documents, to nazwa pakietu czy czego?), który spinałby te wszystkie składowe w całość. A tak, to cykl życia np. Header i Body wygląda na zupełnie niezależny.

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