Wątek przeniesiony 2023-01-16 17:36 z C# i .NET przez Riddle.

DDD - jak poprawnie powinien wyglądać model domeny

0

Cześć,

Mam taki case, że mam encje Day, Task, CompletedTask. Próbuje zamodelować domenę.

Logiczne mogłoby się wydawać że agregateRoot bedzie Day ktory ma w sobie taski a taski mają w sobie completedTaski. Z tym, że nie chcę żeby Day był aggregateRoot ze względu na performance.

Więc pomyślałem, że Task będzie agregateRoot i Day też będzie osobnym agregateRoot.

Tylko, że nie do końca wiem czy to jest poprawne bo Task w sobie musi mieć id encji Day. Więc pomimo tego, że jest osobnym agregateRoot to i tak "relacja" pomiędzy Day a Task będzie właśnie przez to id.

Mam w obrębie tego mikroswerwisu taką akcję, że leci zdarzenie integracyjne które ma w sobie Id Day, bo Day jest inna reprezentacją encji która istnieje w drugim mikroserwisie. I teraz ja muszę przypisać wszystkim Taskom które mają w sobie nulowe Id -> Day przypisać to id co przyszło w tym evencie.
Więc jedyną opcję jaką widzę to wyciągnać je wszystkie z bazy i dla każdego agregatu (czyli Taska) wykonać metode assignDayId(). Tylko, że to też mi się dziwne wydaje, bo wyciągając listę tasków z bazy muszę w repozytorium mieć takie query => context.Tasks.Where(userId = id && dayId is null)

Wie ktoś jak to powinno być poprawnie zamodelowane?

Dzięki

2

Co oznacza "nie chcę (...) ze względu na performance"?
Co przez to rozumiesz, gdzie dostrzegasz potencjalne problemy, które miałoby to przynieść i dlaczego?

Bo wstępne założenie jest "sensowne", choć żeby to tak naprawdę ocenić przydałby się szerszy kontekst.
No bo jeśli rozumieć "Day" jako top-Level całego modelu, to nie sądzisz, że jest to zbyt ogólne? (No bo jak, to dzień w kontekście konkretnej osoby czy może kraju lub nawet całego świata?).

Inna sprawa, że brzmi to trochę jakbyś próbował to sobie w głowie ułożyć na przykładzie klasycznego "TODO List" i o ile da się na tym bazować, to niekoniecznie jest to dobry kandydat na tego typu modelowanie ze względu na poziom złożoności.

2

Tylko, że to też mi się dziwne wydaje, bo wyciągając listę tasków z bazy muszę w repozytorium mieć takie query => context.Tasks.Where(userId = id && dayId is null)

ale co tu jest takiego dziwnego w tym query?

1
RideorDie napisał(a):

Tylko, że nie do końca wiem czy to jest poprawne bo Task w sobie musi mieć id encji Day. Więc pomimo tego, że jest osobnym agregateRoot to i tak "relacja" pomiędzy Day a Task będzie właśnie przez to id.

A jak inaczej? Wykorzystanie id to jest chyba jedyny sposób na powiązanie ze sobą aggregate root'ów.

Co to znaczy, że taski mają w sobie completedTaski?

2
RideorDie napisał(a):

Wie ktoś jak to powinno być poprawnie zamodelowane?

Nie próbuj nic "modelować", to jest złe podejście.

Zaimplementuj całość, nie używając do tego w ogóle bazy i pokryj całość testami. Potem na samym końcu podepnij to pod bazę używając polimorfizmu.

2
RideorDie napisał(a):

Mam taki case, że mam encje Day, Task, CompletedTask. Próbuje zamodelować domenę.

Skoro data jest prostym polem prawie w każdej bazie danych, to nie widzę powodu aby z Day robić encję.
No chyba że Day to taki samosabotaż, czyli Day wcale nie oznacza daty.

2

Encje w DDD to coś unikalnego, co ma tożsamość. Więc Task raczej będzie encją.

Pytanie, czy Day powinien być encją? Jeśli Day ma oznaczać np. 14 stycznia 2023 to w zasadzie wg DDD bardziej by pasowało do "value object" (ponieważ dwa obiekty mające wartość 14 stycznia 2023 będą równe).

Chociaż coś tam piszesz, że Day ma zawierać pewne dane w środku, czyli Day u ciebie to nie tylko sam kalendarzowy dzień, ale raczej coś, co trzyma Taski w środku? Taka kartka z kalendarza bardziej?

1

@LukeJL, Zawsze może być osobny opis dnia, np date + info ale to niezależna tabela

4

Ale co ma DDD wspólnego z bazami danych w ogóle? Cała implementacje powinna być niezależna od bazy.

Chyba tylko słowo "encja" jest wspólne, ale encja z DDD i encja z baz danych to są dwie zupełnie różne rzeczy.

0

@Riddle, to był dodatkowy argument, bo wydawało mi się jasne że w C# data to jeden z typów wbudowanych.

3

Jak zgaduję, to z Day nie chodzi o datę tylko o kolekcję tasków w danym dniu + jeszcze jakieś dodatkowe informacje.

1

Pragmatyczna odpowiedź na Twój post powinna być taka, że DDD używa się tam, gdzie ono rzeczywiście ma sens i daje wartość. Problemu, który próbujesz rozwiązać nie powinno się rozwiązywać za pomocą DDD, ale rozumiem że to projekt do ćwiczeń więc dajmy się ponieść emocjom i zróbmy to w DDD!

Z tym, że nie chcę żeby Day był aggregateRoot ze względu na performance.
Więc pomyślałem, że Task będzie agregateRoot i Day też będzie osobnym agregateRoot.

Wróć do książki o DDD (polecam w pierwszej kolejności Vernona, a potem Evansa) i poczytaj czym jest aggregate root. W skrócie AR to taki twór który trzyma granice transakcji oraz pilnuje niezmienników. Granica transakcji, czyli graf obiektów reprezentowany przez AR powinien być zapisany w ramach jednej transakcji. Transakcji nie tylko technicznej (np na bazie danych) ale też biznesowej.

W pseudo kodzie mogłoby to wyglądać tak, że pobierasz AR za pomocą repozytorium (zwykle taki AR zawiera w sobie kilka encji i obiektów wartości), wykonujesz na nim logikę, która przykładowo zmienia coś w encjach trzymanych w ramach danego agregatu, np. może to być zmiana, który przełoży się na aktualizację pól w kilku tabelach bazodanowych), zapisujesz zmiany korzystając z repozytorium i zadaniem repo jest sprawić, aby wszystkie aktualizacje pod spodem się wykonały, albo żadna (w przypadku błędu) w ramach tej samej transakcji.

Podobnie jest z pilnowaniem niezmienników, czyli pewnych reguł biznesowych. Książkowy przykład to przeliczenie ceny po dodaniu/usunięciu produktów i wyliczenie rabatu.

Popatrz więc na swoją domenę i jej problem space i spróbuj dojść do tego, który z twoich obiektów pasuje do tej definicji i zrobić z niego AR.

Co do wydajności to dobrze podświadomie czujesz, że AR może być tłusty, ale da się to rozwiązać przez:

  • refaktoryzację modelu dziedziny i wydzielenie poziomów modelu dziedzinowego (capability, domain knowdledge, operations, policies)
  • zastosowanie CQRS, czyli separację modeli służących do zapisu danych (model dziedziny), od modelu odczytowego. Przykładowo chcąc pobrać listę zamówień użytkownika, nie targamy z bazy n agregatów, aby wyświetlić 4 wartości w tabelce na UI, tylko stosujemy dedykowane modele odczytowe (view models/dto).

bo wyciągając listę tasków z bazy muszę w repozytorium mieć takie query => context.Tasks.Where(userId = id && dayId is null)

Ale dlaczego zamiast pojedyńczego agregatu wyciągasz listę tasków? Repozytorium oparte DDD powinno mieć dwie metody Find oraz Save

public Task<AggregateRoot> FindAsync(Id id);
public Task SaveAsync(AggregateRoot aggregateRoot);

Wtedy pobierasz agregat który ma przykładowo listę Tasków, wykonuje na nich logikę biznesową i zapisuje zmiany na tych wszystkich Taskach w jednej transakcji.

Jak myślisz, który z twoich obiektów spełnia definicję AR?

1
markone_dev napisał(a):

Pragmatyczna odpowiedź na Twój post powinna być taka, że DDD używa się tam, gdzie ono rzeczywiście ma sens i daje wartość. Problemu, który próbujesz rozwiązać nie powinno się rozwiązywać za pomocą DDD...

Czemu w sumie?

2
Riddle napisał(a):

Czemu w sumie?

Ja nie widzę w problemie OPa logiki biznesowej, która wymagałaby zaprzęgnięcia całego stacku DDD i trudności implementacyjnych jakie się z tym wiążą. Nie każdy problem biznesowy trzeba rozwiązywać za mocą taktycznego DDD czyli wspomnianych building blocków jak: Agregaty, Encje, Obiekty Wartości, Repozytoria, Polityki, Fabryki, Serwisy Domenowe, itd. Czasem prosta architektura encja-serwis będzie ok, a nawet lepsza. Ba! Samo DDD mówi o tym, że w dużych aplikacjach/systemach wyróżniamy poddziedziny: Główne (Core Subdomains), Wspierające (Supporting Subdomains) i generyczne (Generic Subdomains). I to właśnie problemy z obszaru dziedziny głównej są najlepszym kandydatem do stosowania zaawansowanych technik DDD. Dziedziny wspierające z definicji już takiego wymagania nie mają, a generyczne to w ogóle, bo implementacje dziedzin generycznych się kupuje a nie pisze i temat załatwia się integracją.

Powyższe nie dotyczy się strategicznych technik DDD takich jak cały proces odkrywania języka i wszystkiego co wiąże się z destylacją poddziedzin. Te warto stosować zawsze, niezależnie od skali projektu.

0

Cześć,

Encja Day jest reprezentacją dnia w którym muszę wykonać określone zadania w obrębie większego zbioru do którego należą te zadania - nazwijmy go celem. A więc przechowuje też w encji Day id do tego zbioru.

Problem z szybkością działania mam taki, że chcę żeby operacje takie jak np. oznaczenie zadania jako wykonane na agregacie wykonywały się jak najszybciej.

Czyli jeżeli zrobiłbym Day agregate rootem i będzie w sobie zawierał załóżmy 100 tasków. Każdy z tych tasków ma w sobie listę subtasków i jeszcze listę o nazwie taski wykoane. Więc trochę hardkor imo. W dodatku ilosć subtasków i tasków wykonanych dla danego taska codziennie rośnie.

Miałem to zrobione w ten sposób że

Day: AggregateRoot

Task: AggregateRoot
List<Subtask> subtasks;
List<CompletedTasks> completedTasks;

Wg mnie, to dalej nie jest optymalny model bo muszę ładować te dwie listy, więc skłaniałbym się do tego żeby rozbić to jeszcze bardziej i wtedy CompletedTask byłby również aggregate rootem.

Czyli

Day: AggregateRoot

CompletedTask: AggregateRoot

Task: AggregateRoot
List<Subtask> subtasks

Co myślicie o tym?

Wywale wtedy metodę z agregatu Task odpowiadająca za oznaczenie zadania jako wykonane. Tylko, że wtedy przy oznaczeniu zadania jako wykonane będę musiał najpierw sprawdzić czy w bazie nie ma już takiego zadania oznaczonego jako wykonane. Czyli muszę z repozytorium CompletedTask szukać czy jest jakis rekord po takim warunku - > _completedTasks.FirstOrDefault(x => x.SubTaskId == subTask.Id && x.FinishedAt.Date == now.Date), i usunac albo dodac jezeli nie ma.
Tyle ze dodanie metody w repo która szuka po takim warunków przestaje mi pasować z teorią odnosnie rpozytorium dla agregate root ktorym jest completedTask

Wiem trochę przerost formy nad treścią robienie do tego DDD ale powiedzmy że Task ma w sobie 3 metody z logiką biznesową które cos tam robią. Ale to nie ma znaczenia zostawmy ten temat.

Z tego co wiem to repozytoria robi się tylko dla agregatów no i te repozytoria mająw sobie zawierać tylko proste metody do wyciagniecia agregatu i zapisania. Więc wszelkie wyciąganie agregatów na podstawie jakiś warunków w repozytorium powinno z miejsca dawać sygnał, że coś jest nie tak. Zgodzice się?

Ale powiedzmy, że potrzebuje przypisać te zadania które dodałem sobie do jakiegoś zbioru.
Więc no muszę w repozytorum dla Taska dodać kolejną metoda która będzie wyciagala tylko te taski co nie są jeszcze przypisane do żadnego zbioru. Tym samym chyba zaczynam naruszać zasadę jak to pownno byc zrobione.

Ktoś wspomniał, że nie powninenem się interesować bazą jak robię DDD - niby tak. U mnie model domenowy jest jednocześnie modelem bazodanowym

1
RideorDie napisał(a):

Co myślicie o tym? Wywale wtedy metodę z agregatu Task odpowiadająca za oznaczenie zadania jako wykonane.

Dlaczego? Jak dla mnie taka metoda powinna być, i zwracać CompletedTask na podstawie tego Task. Potem CompletedTask zapisujesz, a Task usuwasz.

Z tego co wiem to repozytoria robi się tylko dla agregatów no i te repozytoria mająw sobie zawierać tylko proste metody do wyciagniecia agregatu i zapisania. Więc wszelkie wyciąganie agregatów na podstawie jakiś warunków w repozytorium powinno z miejsca dawać sygnał, że coś jest nie tak. Zgodzice się?

Nie, czemu? To normalne, że repozytorium zwraca Ci tylko wybrane obiekty.

1

Problem z szybkością działania mam taki, że chcę żeby operacje takie jak np. oznaczenie zadania jako wykonane na agregacie wykonywały się jak najszybciej.

Dlaczego? Czyżby synchroniczny strzał z frontendu który sprawia, że strona się "zawiesza" i pojawia się spinner na czas zapisywania danych, a użytkownik sobie cierpliwie czeka?

Jeżeli performance jest twoim priorytetem to może DDD z warstwą ORM-a nie jest najlepszym rozwiązaniem w tym przypadku? Z doświadczenia czasem logika przy zapisie agregatu może być skomplikowana i trwać dłużej wtedy najlepiej wydzielić to do osobnego procesu wrzucając komendę na kolejkę i obsłużyć to dedykowanym procesem w tle, który powiadomi główną aplikację o tym, że zadanie się wykonało i wtedy userowi pojawi się wiadomość w UI, że zakończono przetwarzanie zadania.

Dlatego wrócę do mojego posta wyżej i powtórzę, że DDD pasuje do twojego problemu jak "pięść do nosa". Zadaniem taktycznego DDD jest rozwiązywanie problemów biznesowych. Każdy kto pracował trochę z DDD ci powie, że zadaniem DDD nie jest rozwiązywanie problemów wydajnościowych i że czasem aby rozwiązać problem taki jak twój (tłusty agregat), trzeba skomplikować rozwiązanie dodając przetwarzanie asynchroniczne w tle oparte o na przykład kolejki. Można też odchudzić agregat rozbijając go na mniejsze, ale wtedy dochodzi problem spójności danych bo w ramach jednej transakcji biznesowej będziesz miał dwie osobne transakcje bazodanowe (po jednej na agregat, napisałem o tym niżej) i co gdy transkacja T1 się wykona a T2 rzuci błędem? Będzie boleć bo ani baza ani ty nie zrobi automatycznie ROLLBACK na bazie i przywróci poprzedni stan. Oj nie :)

Wg mnie, to dalej nie jest optymalny model bo muszę ładować te dwie listy, więc skłaniałbym się do tego żeby rozbić to jeszcze bardziej i wtedy CompletedTask byłby również aggregate rootem.

Dalej nie rozumiesz czym jest AR. Dobry AR nie ma silnej zależności do innego AR (romb w UML-u). Innymi słowy jak masz dwa agregaty A1 i A2 to A2 ma tylko ID agregatu A1 przez co nie możesz ich zapisać ORM-em w jednej transakcji. I tak ma być bo jak napisałem wyżej jeden AR jedna transakcja.

Z tego co wiem to repozytoria robi się tylko dla agregatów no i te repozytoria mająw sobie zawierać tylko proste metody do wyciagniecia agregatu i zapisania. Więc wszelkie wyciąganie agregatów na podstawie jakiś warunków w repozytorium powinno z miejsca dawać sygnał, że coś jest nie tak. Zgodzice się?

Dokładnie tak jest bo agregat to nie jest model odczytowy, żeby go wyciągać by Id, by Email, by Start and End Date i tak dalej.

Jak masz polisę ubezpieczeniową, którą chcesz zmodyfikować to kluczem do wyciągnięcia polisy (w zależności) od designu może być

a) Id polisy rozumiane jako unikalny identyfikator np GUID
b) coś customowo wygenerowane przez system jak HUD-2332-2111-23-1

I wtedy twoje repozytorium przyjmuje takie ID (które nie musi i często nie powinno być kluczem PK bazy danych), zwraca ci agregat na którym wykonujesz logikę i zapisujesz go z powrotem do bazy.

Jak twój AR jest duży to możesz rozbić go na mniejsze ale wtedy obsługujesz je niezależnie, czyli jeżeli masz dwa agregaty A1 i A2, to przykładowo

  1. Pobierasz A1 z bazy za pomocą repozytorium R1
  2. Wykonujesz na A1 logikę
  3. Zapisujesz A1
  4. Publikujesz zdarzenie domenowe E1
  5. W reakcji na zdarzenie E1 listener agregatu A2 odpala dedykowany dla A2 serwis lub handler S2
  6. S2 pobiera z repozytorium R2 agregat A2
  7. Wykonuje na A2 logikę biznesową
  8. Zapisuje A2 do bazy za pomocą repozytorium R2
  9. Zastanawiasz się co zrobić jak zapis agregatu A2 się wywali i w systemie powstaną niespójne dane.

Ktoś wspomniał, że nie powninenem się interesować bazą jak robię DDD - niby tak. U mnie model domenowy jest jednocześnie modelem bazodanowym

I to jest złe podejście bo model dziedzinowy powinien wiedzieć jak najmniej o bazie danych bo jego zadaniem jest rozwiązywanie problemów biznesowych a nie bazy danych, dlatego niektórzy stosują podejście, gdzie mają osobne modele: bazodanowy i dziedzinowy. Czasem się to sprawdza, czasem nie. Zależy od use case'a.

Podsumowanie. Jak widzisz stosowanie DDD zgodnie ze sztuką wprowadza duży narzut pracy i niesamowicie komplikuje całe rozwiązanie, dlatego DDD stosuje się tam, gdzie zysk >> koszty potrzebne na jego implementację które nie są małe. W większości projektów robienie DDD sprawia że koszty >> zysk biznesowy bo zamiast skupiać się na rozwiązywaniu problemu biznesowego i dostarczeniu funkcjonalności jak najszybciej, żeby zarabiała na siebie pieniądze skupiamy się na rozwiązywaniu problemów technicznych uprawiając tzw "onanizm techniczny".

Dlatego oszczędź sobie czasu i nerwów, zrób anemiczny model dziedziny, a logikę umieść w dedykowanych serwisach bo twój przypadek to nie jest use case dla DDD.

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