Ładowanie agregatów, granice agregatów, encje - kilka pytań

3

Cześć, z tego co rozumiem, to Agregaty są jednostkami pracy hermentyzującymi reguły biznesowe. Czyli mówiąc po chłopsku zawiera on wszystkie pola, które potrzebujemy aby wykonać w środku całą ifologię i obliczyć wymagany wynik - nie więcej. Dzięki temu też zyskujemy wysoką kohezję klasy, czyli w idealnym przypadku wszystkie metody korzystają ze wszystkich pól tej klasy (wiadomo, że ciężko osiągnąć 100% tego, ale wiadomo o co chodzi).
Do ładowania sobie agregatów (najczęściej za pomocą jakiś ID) służą natomiast Repozytoria.

Mamy sobie standardowe wymaganie biznesowe aby dodać produkt do zamówienia. Sobótka zrobił to tu https://bottega.com.pl/pdf/materialy/ddd/ddd1.pdf (mówię o str. 45) w ten sposób, że ładuje encje produktu z ProductRepository, ładuje agregat zamówienia czyli Order z OrderRepository i do Orderu dodaje ten załadowany product pod spodem wykorzystując jakieś mapowanie JPA, czyli @ManyToOne. Nie pasuje mi tu bardzo to, że z tego co wyczytałem to repozytoria służą do ładowania i zapisywania agregatów, a nie encji. Jeśli mamy sobie fasadę jako wejście do modułu to nie powinno ono zawierać tylko jednego repozytorium związanego z agregatem? Bo ten jego kod z kilkoma repozytoriami zaczyna wyglądać jak kandydat na 8tysięcznik, gdzie serwis ma 10 repozytoriów.
Czy to nie powinno wyglądać tak, że agregat Order wygląda raczej w tym stylu jak ponizej? (kod będzie w Javce)

public class OrderLine {
    private String productId;
    private int quantity;
}

public class Order {
    
    private final Set<OrderLine> orderLines;
    
    public void addProduct(int quantity, String productId) {
        // logika sprawdzenia ilości i ewentualnego dodania nowego id do listy
    }
}

W agregacie mamy już załadowane dane o produktach, które potrzebujemy do ifologii w środku i zapisujemy po prostu czystą informację biznesową czyli productId wraz z ilością. Czy to nie tak powinno wyglądać? Agregat Product zawiera wyłącznie to co potrzebuje i potem pod spodem sqlka z updatem też będzie mniejsza.

Druga sprawa o którą chciałem dopytać to granice agregatów. Załóżmy, że użytkownik może dodawać i updatować jakiś Item do sklepu internetowego. Mamy sobie taki agregat:

public class Item {

    private ItemId id;
    private Title title;
    private Description description;
    private ItemStatus status;
    private Price price;
    private LocalDateTime soldDate;


    public Item(ItemId id, ItemUpdateForm form) {
        this.id = id;
        this.title = form.getTitle();
        this.description = form.getDescription();
        this.status = ItemStatus.NEW;
        this.price = form.getPrice();
        this.soldDate = null;
    }

    public void update(ItemUpdateForm form) {
        this.title = form.getTitle();
        this.description = form.getDescription();
        this.price = form.getPrice();
    }
}

No i pewnie większosć powie, że to nie jest agregat, bo generalnie nie ma żadnych reguł biznesowych.
Ale zaraz dodamy jakąś metodę biznesową z logiką, coś np jak:

public void markAsSold(TimeService timeService) {
        if (status != ItemStatus.SOLD && status != ItemStatus.BLOCKED) {
            status = ItemStatus.SOLD;
            soldDate = timeService.now();
        }
    }

I co, to już się nagle robi agregat, bo jest w tym logika biznesowa?

Chyba, że Item powinienem zostawić jako jakiś prosty obiekt i nie myśleć o nim w kategoriach agregatu, a do zarządzania stanami wydzielić jakiś osobny obiekt, który już będzie agregatem? No bo faktycznie metoda markAsSold i inne typu markAsBlocked nie będą prawdopodobnie potrzebowały w ogóle takich pól jak title czy description.
Jak to wtedy jest?

0

Wydaje mi się, że DDD może nie pasować do użycia z JPA, pokusiłbym się o stwierdzenie, że JPA łatwiej użyć w połączeniu z Anemic Domain Model niż DDD. Chociaż niektórzy ADM uważają jako antypattern.

Wyobraźmy sobie klasę Order, gdzie mamy metodę void accept(). Logika biznesowa mówi o sprawdzeniu stanu, zatwierdzeniu zamówienia oraz wysłaniu maila do użytkownika. Czy encja JPA powinna w sobie mieć logikę wysyłania maili? Technicznie to ciężko do encji JPA wstrzyknąć serwis odpowiedzialny za wysyłkę maili. To pojawia się rozwiązanie techniczne, żeby metoda do akceptacji zamówienia przyjmowała parametr i teraz wygląda tak: void accept(EmailService emailService). Nie po to powstawało wstrzykiwanie zależności, aby nimi żonglować pomiędzy wołaniami metod.

A gdyby pójść w stronę ADM to mamy tak, że tworzymy sobie OrderAcceptService z metodą void accept(Order order) i w środku sprawdzamy stan obiektu, ustawiamy statusy oraz za pomocą wstrzyknięcia EmailService do OrderAcceptService wysyłamy maila w tej metodzie. Encje JPA stają się tylko workiem na dane, zero logiki biznesowej w nich, tylko w serwisach. Encje stają się danymi, serwisy wykonują logikę biznesową, następuje separacja danych od kodu.

Przychodzi mi do głowy jeszcze jeden pomysł, żeby owrappować taki obiekt jak encja JPA w takie coś jak np. OrderDDD, ta klasa jako konstruktor przyjmuje encję Order i np. EmailService, zawołanie metody void accept() na tej klasie poustawia stany zatwierdzenia zamówienia Order oraz wyśle maila. I to bardzo podobnie wygląda jak kontrolery, tylko zostały nazwane inaczej.

1
Bambo napisał(a):

Nie pasuje mi tu bardzo to, że z tego co wyczytałem to repozytoria służą do ładowania i zapisywania agregatów, a nie encji.

Dlaczego uważasz, że Product to nie jest agregat?

Jeśli mamy sobie fasadę jako wejście do modułu to nie powinno ono zawierać tylko jednego repozytorium związanego z agregatem?

Ale dlaczego uważasz, że tak powinno? Nieraz logika biznesowa wymaga użycia wielu agregatów.

I co, to już się nagle robi agregat, bo jest w tym logika biznesowa?

Agregat to po prostu zbiór powiązanych ze sobą encji, w encjach jest logika biznesowa. Ale czemu sam Item miałby być agregatem?

ivo napisał(a):

o pojawia się rozwiązanie techniczne, żeby metoda do akceptacji zamówienia przyjmowała parametr i teraz wygląda tak: void accept(EmailService emailService). Nie po to powstawało wstrzykiwanie zależności, aby nimi żonglować pomiędzy wołaniami metod.

Podając EmailService w parametrze metody używasz właśnie wstrzykiwania zależności.

A gdyby pójść w stronę ADM to mamy tak, że tworzymy sobie OrderAcceptService z metodą void accept(Order order) i w środku sprawdzamy stan obiektu, ustawiamy statusy oraz za pomocą wstrzyknięcia EmailService do OrderAcceptService wysyłamy maila w tej metodzie. Encje JPA stają się tylko workiem na dane, zero logiki biznesowej w nich, tylko w serwisach. Encje stają się danymi, serwisy wykonują logikę biznesową, następuje separacja danych od kodu.

Taki wzorzec nazywa się skrypt transakcji, no i z DDD ma mało wspólnego, z programowaniem obiektowym zresztą też - co nie znaczy, że to złe podejście. Czasami jest wystarczające.

1

@ivo:
Ale przecież nikt Ci nie każe wsadzać JPA do Twojej warstwy domenowej. JPA znajduje się w warstwie infrastruktury gdzie implenetujesz Repozytoria. Masz sobie agregat Order bez żadnych adnotacji JPA, Możesz przecież dać sobie pole EventPublisher i je wstrzykiwać w Fabrykach i po stronie implementacji Repo przecież. Wtedy po stronie implementajci repo jeśli korzystasz z JPA masz coś takiego jak OrderJpaEntity, który jest zwykłą strukturą danych bez metod biznesowych .. i z adnotacjami. A to, że 99% wsadza JPA do domeny i u nich Encja domenowa = Encja JPA to inna sprawa ;p

@somekind
Uważam, że Product nie jest agregatem, bo nie zawiera w sobie przecież żadnych reguł biznesowych. Nawet w tym repo Sławka Sobótki jest on tam chyba oznaczony adnotacją @DomainEntity.

No jak dla mnie moduł to jest pojedyńcza odpowiedzialnośc, która powinna operować na jednej logicznej jednostce będącej właśnie agregatem. Będąc purystą w transakcji nie powinieneś zapisać więcej niż 1 agregatu. Jeśli wykonując jakąś logikę biznesową na agregacie musisz odpytać o coś inny agregat albo coś zapisać w innych agregatach to możesz to zrobić poprzez wywołanie odpowiedniej metody fasady innego modułu albo wyemitować event, na który zareagują inne moduły/agregaty.

No, ale jeśli po stronie Item robią Ci się jakieś reguły biznesowe to staje się on agregatem no nie? Wszystko zależy jakie są Twoje bounded contexty. W sumie z tym itemem to jest tak trochę dziwnie, bo z jednej strony to jest struktura danych, a drugiej ma jakieś biznesowe zachowania. Nie wiem jak to traktować.

5
Bambo napisał(a):

Cześć, z tego co rozumiem, to Agregaty są jednostkami pracy hermentyzującymi reguły biznesowe. Czyli mówiąc po chłopsku zawiera on wszystkie pola, które potrzebujemy aby wykonać w środku całą ifologię i obliczyć wymagany wynik - nie więcej. Dzięki temu też zyskujemy wysoką kohezję klasy, czyli w idealnym przypadku wszystkie metody korzystają ze wszystkich pól tej klasy (wiadomo, że ciężko osiągnąć 100% tego, ale wiadomo o co chodzi).

Agregat to przede wszystkim granica transakcyjności (ang. transaction boundary). Cała reszta która z tego się wywodzi to pewien efekt uboczny. Kluczem przy projektowaniu agregatów jest przede wszystkim ta jedna zasada związana z transakcyjnością agregatu. Samo powiedzenie że agregat zawiera wszystkie potrzebne pola jest błędne pod względem technicznym, bo agregat to zgrupowanie encji i value objects które wchodzą w skład tej transakcyjności. Oczywiście, oznacza to że root agregatu będzie posiadać pola dające dostęp do innych obiektów ale znów- jest to efekt a nie przyczyna.

Do ładowania sobie agregatów (najczęściej za pomocą jakiś ID) służą natomiast Repozytoria.

Mamy sobie standardowe wymaganie biznesowe aby dodać produkt do zamówienia. Sobótka zrobił to tu https://bottega.com.pl/pdf/materialy/ddd/ddd1.pdf (mówię o str. 45) w ten sposób, że ładuje encje produktu z ProductRepository, ładuje agregat zamówienia czyli Order z OrderRepository i do Orderu dodaje ten załadowany product pod spodem wykorzystując jakieś mapowanie JPA, czyli @ManyToOne. Nie pasuje mi tu bardzo to, że z tego co wyczytałem to repozytoria służą do ładowania i zapisywania agregatów, a nie encji.

Nie popełniałbym błędu przedstawiania encji w opozycji do agregatów, bo encje mogą wchodzić w skład agregatu. Zazwyczaj encja jest właśnie rootem agregatu. No chyba że robimy całkowite rozróżnienie na obiekty bazodanowe (encje) i nie-bazodanowe, a więc mapujemy z jednych na drugie. Wtedy to miałoby większy sens. Słusznie jednak masz wątpliwości, i to co opisujesz wydaje się błędne.

Czy to nie powinno wyglądać tak, że agregat Order wygląda raczej w tym stylu jak ponizej? (kod będzie w Javce)

public class OrderLine {
    private String productId;
    private int quantity;
}

public class Order {
    
    private final Set<OrderLine> orderLines;
    
    public void addProduct(int quantity, String productId) {
        // logika sprawdzenia ilości i ewentualnego dodania nowego id do listy
    }
}

Tak, to jest dobry i zresztą kanoniczny przykład. Dodajemy produkt do zamówienia (chociaż w myśl zasad nazewnictwa w DDD masz tu pewien błąd, bo masz operację addProduct która w rzeczywistości tworzy order line). W tym przypadku Product może również być pełnoprawnym agregatem, ale nie ma to znaczenia bo do agregatu Order dodajemy tylko ID agregatu Product, a więc granica transakcyjności nie zostaje złamana. Agregaty powinno odnosić się do innych agregatów tylko po ich ID.

W agregacie mamy już załadowane dane o produktach, które potrzebujemy do ifologii w środku i zapisujemy po prostu czystą informację biznesową czyli productId wraz z ilością. Czy to nie tak powinno wyglądać? Agregat Product zawiera wyłącznie to co potrzebuje i potem pod spodem sqlka z updatem też będzie mniejsza.

Zgadza się. Chociaż chcę zaznaczyć że takie szczegóły jak rozmiar polecenia SQL w DDD to sprawy drugorzędne. Inaczej mówiąc, nie należy rozpatrywać tego jako zaleta w kontekście DDD bo nie ma to znaczenia dla procesu biznesowego który się modeluje.

Druga sprawa o którą chciałem dopytać to granice agregatów. Załóżmy, że użytkownik może dodawać i updatować jakiś Item do sklepu internetowego. Mamy sobie taki agregat:
...
No i pewnie większosć powie, że to nie jest agregat, bo generalnie nie ma żadnych reguł biznesowych.
Ale zaraz dodamy jakąś metodę biznesową z logiką, coś np jak:

public void markAsSold(TimeService timeService) {
...
    }

I co, to już się nagle robi agregat, bo jest w tym logika biznesowa?

Chyba, że Item powinienem zostawić jako jakiś prosty obiekt i nie myśleć o nim w kategoriach agregatu, a do zarządzania stanami wydzielić jakiś osobny obiekt, który już będzie agregatem? No bo faktycznie metoda markAsSold i inne typu markAsBlocked nie będą prawdopodobnie potrzebowały w ogóle takich pól jak title czy description.
Jak to wtedy jest?

To zależy. Możesz to potraktować jako oddzielny byt, w całkowicie oddzielnym, CRUD-owym kontekście. Z drugiej strony możesz uznać że całe zarządzanie magazynem i rzeczami na stanie ma również skomplikowane procesy i zasady biznesowe (bardzo prawdopodobne). Zwróć uwagę że omawiamy fikcyjny przykład, stąd też ciężko wysunąć ostateczne wnioski. DDD to nie tylko zagadnienia techniczne- koniec końców w rzeczywistości odpowiedź przyszła by Ci łatwiej, ponieważ to ekspert domenowy dyktowałby jak w rzeczywistości wyglądają procesy. Programista w oparciu o DDD tylko by je przelał na kod.

Dodam jeszcze że w tym fikcyjnym modelu bardzo prawdopodobne że każdy OrderLine dodany do zamówienia miałby również cenę, ponieważ ta cena może się różnić od ceny katalogowej produktu.

1
Aventus napisał(a):

Agregat to przede wszystkim granica transakcyjności (ang. transaction boundary). Cała reszta która z tego się wywodzi to pewien efekt uboczny. Kluczem przy projektowaniu agregatów jest przede wszystkim ta jedna zasada związana z transakcyjnością agregatu. Samo powiedzenie że agregat zawiera wszystkie potrzebne pola jest błędne pod względem technicznym, bo agregat to zgrupowanie encji i value objects które wchodzą w skład tej transakcyjności. Oczywiście, oznacza to że root agregatu będzie posiadać pola dające dostęp do innych obiektów ale znów- jest to efekt a nie przyczyna.

Granica transakcyjności jak rozumiem oznacza, że w 1 transakcji zapiszę ten 1 agregat i nie więcej tak? No dobra, ale z drugiej strony nie wszystkie pola, które potrzebuję do wykonania operacji muszę zapisać. WIęc tu się pojawia sprzeczność. Załóżmy, że agregat ma pola a, b, c i w metodzie jest coś takiego:

a = b + c

b i c nie zapisuję w tym momencie, one mi były tylko potrzebne do wykonania logiki biznesowej.

Tak, to jest dobry i zresztą kanoniczny przykład. Dodajemy produkt do zamówienia (chociaż w myśl zasad nazewnictwa w DDD masz tu pewien błąd, bo masz operację addProduct która w rzeczywistości tworzy order line). W tym przypadku Product może również być pełnoprawnym agregatem, ale nie ma to znaczenia bo do agregatu Order dodajemy tylko ID agregatu Product, a więc granica transakcyjności nie zostaje złamana. Agregaty powinno odnosić się do innych agregatów tylko po ich ID.

Jasne, o nazewnictwie już tu nie wspominałem, addProduct to nie jest dobra nazwa ;p

Zgadza się. Chociaż chcę zaznaczyć że takie szczegóły jak rozmiar polecenia SQL w DDD to sprawy drugorzędne. Inaczej mówiąc, nie należy rozpatrywać tego jako zaleta w kontekście DDD bo nie ma to znaczenia dla procesu biznesowego który się modeluje.

No właśnie mówiłem o tym w kontekście zalety.

To zależy. Możesz to potraktować jako oddzielny byt, w całkowicie oddzielnym, CRUD-owym kontekście. Z drugiej strony możesz uznać że całe zarządzanie magazynem i rzeczami na stanie ma również skomplikowane procesy i zasady biznesowe (bardzo prawdopodobne). Zwróć uwagę że omawiamy fikcyjny przykład, stąd też ciężko wysunąć ostateczne wnioski. DDD to nie tylko zagadnienia techniczne- koniec końców w rzeczywistości odpowiedź przyszła by Ci łatwiej, ponieważ to ekspert domenowy dyktowałby jak w rzeczywistości wyglądają procesy. Programista w oparciu o DDD tylko by je przelał na kod.

Z tym Item to nie do końća fikcyjny przykład, bo mam w robocie uber serwise ItemService, którego dwie metody to typowo addItem i updateItem, które tworzą albo updatują Item wraz z jakąś walidację na podstawie sporego forma przychodzącego z frontu. Tak reguł biznesowych poza walidacją za bardzo nie ma - na końcu jedynie emitowane są eventy item.added oraz item.updated.

Poza tymi dwoma metodami są jednak już mniejsze biznesowe metody, które wykonują jakąś logikę biznesową - przeliczają cenę, sprawdzają status itd - dlatego właśnie się zastanawiałem jak to ugryźć, żeby się pozbyć takiego wielkiego serwisu jak ItemService i tych metod, które mają po 20 linijek w większosci. Po prostu analizuję, czy ten Item od create i update na podstawie forma to nie jest jakaś część crudowa, a dodatkowo istnieje "drugi" Item być może inaczej nazwany gdzie już DDD ma miejsce. Od razu zaznaczam, że nie ja jestem jego autorem tego uber serwisu :D

@Aventus jeszcze pytanie odnośnie tym czym są same agregaty. Czy agregat zawsze musi oznaczać takie zjawisko, że jak go usunę to usuwam wszystkie jego encje i ogólnie zależności? Czy agregat powinien być usuwalny?

Przez dłuższy czas analizowałem jak zamodelować agregat dla aplikacji, która umożliwia użytkownikom porównywanie produktów. Założmy masz sobie jakieś ciuchy i użytkownik w apce dostaje dwa zdjęcia jakiś losowych sukienek i klika, która mu się bardziej podoba - te zestawienia są losowo generowanie przez backend, client dostaje tylko listę jsonów ze ID ciucha oraz URL zdjęcia ciucha.
User sobie w apce klika, która ładniejsza i idzie request do backendu z ID_1, ID_2 oraz ID_WIN.

To co musi backend zrobić to sprawdzić jakie obecnie mają rankingi te ciuchy, przeliczyć je jakimś algorytmem ELO i zapisać.

Najprostsze tutorialowe podejście to obiekt Cloth z jakimś rankingiem i jeśli chcemy być pro to jakaś RankPolicy, która jest interfejsem i pod spodem chowa algorytm ELO.
W metodze serwisowej standardowo pobieramy Cloth z ID_1, Cloth z ID_2, sprawdzamy, który z nich wygrał, updatujemy oba rankingi i zapisujemy do repo Cloth z ID_1 i Cloth z ID_2. Oczywiście na metodzie serwisowej mamy transakcję, bo wiadomo .. trzeba oba rankingi na raz ogarnąć.

Ja rozkminiłem inne rozwiązanie. Stworzyłem agregat Comparision:

class Comparision {
    
    private final ComparisionId id;
    
    private final Rank rank1;
    
    private final Rank rank2;
    
    public void recalculate(long winClothId, RankPolicy rankPolicy) {
        // logic
    }
}

class ComparisionId {
    
    private int cloth1Id;
    
    private int cloth2Id;
}

class Rank {
    
    private int value;
    
}

Ładując Comparision pod spodem z DB muszę wykonać 2 selecty do tabeli CLOTH i złożyć ten agregat. Podobnie przy zapisaniu robię dwa updaty - ale zachowuję zasadę granic trasakcyjności :) Zapisuję 1 agregat. ID tego agregatu to nie jest zwykły int czy string tylko złożony obiekt. Wydaje mi się, że właśnie to jest taki mega dobry przykład gdzie model domenowy totalnie jest inny niż model bazodanowy. Bo większości przypadków agregat to prawie to samo co wiersz w tabeli. Tu mam nietrywalny przypadek.

Jedynie czego nie mogę zrobić z tym agregatem to go usunąć - bo tak jakby fizycznie w bazie nie jest persystowane nic takiego jak porównanie. Jest persystowany wynik tej operacji, ale z punktu widzenia DDD to jest szczegół, więc wydaje mi się, że to całkiem spoko, ale nie wiem co uważasz?

4
Bambo napisał(a):

Granica transakcyjności jak rozumiem oznacza, że w 1 transakcji zapiszę ten 1 agregat i nie więcej tak?

Drobne sprostowanie- tu chodzi o transakcje w rozumieniu transakcji biznesowej, a nie transakcja po stronie bazy danych. W Twoim przykładzie dodanie produktu do zamówienia jest taką transakcją, której strzeże agregat. Jeśli zasady biznesowe nie zostają złamane to agregat akceptuje polecenie (transakcja się powiodła), w przeciwnym razie je odrzuca (transakcja się nie powiodła). W tym pierwszym przykładzie dochodzi do zapisania agregatu co może ze sobą wiązać transakcję bazodanową, ale to oddzielna kwestia. W drugim przypadku prawdopodobnie nie dojdzie do zapisu bo agregat odrzucił polecenie. Mówię prawdopodobnie bo to zależy od konkretnej domeny. Są przypadki kiedy niepowodzenie jest naturalną częścią agregatu.

No dobra, ale z drugiej strony nie wszystkie pola, które potrzebuję do wykonania operacji muszę zapisać. WIęc tu się pojawia sprzeczność. Załóżmy, że agregat ma pola a, b, c i w metodzie jest coś takiego:

a = b + c

b i c nie zapisuję w tym momencie, one mi były tylko potrzebne do wykonania logiki biznesowej.

Nie widzę tu sprzeczności. Jeśli nie musisz tych pól zapisać to tego nie robisz. Tak samo prawdopodobnie w ogóle nie potrzebujesz tych pól, bo mogą być one zmiennymi lokalnymi.

No właśnie mówiłem o tym w kontekście zalety.

Wiem, a ja powiedziałem że w kontekście DDD taka zaleta nie ma znaczenia :)

Z tym Item to nie do końća fikcyjny przykład, bo mam w robocie uber serwise ItemService, którego dwie metody to typowo addItem i updateItem, które tworzą albo updatują Item wraz z jakąś walidację na podstawie sporego forma przychodzącego z frontu. Tak reguł biznesowych poza walidacją za bardzo nie ma - na końcu jedynie emitowane są eventy item.added oraz item.updated.

Poza tymi dwoma metodami są jednak już mniejsze biznesowe metody, które wykonują jakąś logikę biznesową - przeliczają cenę, sprawdzają status itd - dlatego właśnie się zastanawiałem jak to ugryźć, żeby się pozbyć takiego wielkiego serwisu jak ItemService i tych metod, które mają po 20 linijek w większosci. Po prostu analizuję, czy ten Item od create i update na podstawie forma to nie jest jakaś część crudowa, a dodatkowo istnieje "drugi" Item być może inaczej nazwany gdzie już DDD ma miejsce. Od razu zaznaczam, że nie ja jestem jego autorem tego uber serwisu :D

Ok rozumiem. Z tego co opisujesz to faktycznie nadaje się to do zastosowania DDD, jako że wyraźnie masz zasady biznesowe. Weź natomiast pod uwagę że sama idea agregatu Product może być mylna. W kontekście DDD coś takiego jak produkt może nawet nie istnieć! Ciekawa prezentacja na ten temat tutaj.

@Aventus jeszcze pytanie odnośnie tym czym są same agregaty. Czy agregat zawsze musi oznaczać takie zjawisko, że jak go usunę to usuwam wszystkie jego encje i ogólnie zależności? Czy agregat powinien być usuwalny?

To zależy. Ze względu na samą swoją naturę, agregat to może być coś czego nawet całościowo nie musisz zapisywać do bazy. Tak się dzieje np. przy używaniu event sourcingu. Wtedy instancję agregatu buduje się w locie, odtwarzając po kolei przeszłe eventy emitowane przez dany agregat.

Tak więc nie, niekoniecznie agregat zawsze musi zostać usunięty. Szczególnie że w myśl DDD domenę modeluje się na podstawie procesów biznesowych, a bardzo często w rzeczywistości nie robi się czegoś takiego jak usuwanie. Zamówienie może być *anulowane *(a nie usunięte), produkt może zostać *wycofany *(a nie usunięty), klient może zostać oznaczony jako nieaktywny itd. Jeszcze raz powtórzę że w DDD chodzi nie tylko o technikalia.

Jeśli w projekcie gdzie nie używa się DDD ekspert biznesowy opisze programistom proces, i powie że produkt może zostać wycofany, to programiści zapewne przetłumaczą to sobie na usunięty. I zastosują hard lub soft delete. W kontekście DDD jednak traktuje się dosłownie- jeśli produkt jest wycofywany to tak się to przenosi na kod, i przechowywane dane.

Przez dłuższy czas analizowałem jak zamodelować agregat dla aplikacji, która umożliwia użytkownikom porównywanie produktów. Założmy masz sobie jakieś ciuchy i użytkownik w apce dostaje dwa zdjęcia jakiś losowych sukienek i klika, która mu się bardziej podoba - te zestawienia są losowo generowanie przez backend, client dostaje tylko listę jsonów ze ID ciucha oraz URL zdjęcia ciucha.
...
Ładując Comparision pod spodem z DB muszę wykonać 2 selecty do tabeli CLOTH i złożyć ten agregat. Podobnie przy zapisaniu robię dwa updaty - ale zachowuję zasadę granic trasakcyjności :) Zapisuję 1 agregat.

Tak jak pisałem transakcyjności agregatu nie polega na zapisywaniu go do bazy.

ID tego agregatu to nie jest zwykły int czy string tylko złożony obiekt. Wydaje mi się, że właśnie to jest taki mega dobry przykład gdzie model domenowy totalnie jest inny niż model bazodanowy. Bo większości przypadków agregat to prawie to samo co wiersz w tabeli. Tu mam nietrywalny przypadek.

Jedynie czego nie mogę zrobić z tym agregatem to go usunąć - bo tak jakby fizycznie w bazie nie jest persystowane nic takiego jak porównanie. Jest persystowany wynik tej operacji, ale z punktu widzenia DDD to jest szczegół, więc wydaje mi się, że to całkiem spoko, ale nie wiem co uważasz?

Ja nie wiem czy w tym przypadku jest w ogóle sens mówić o agregatach. Jak sam stwierdzasz, zapisujesz wynik pewnej operacji. Owszem, ta operacja może również zawierać jakąś logikę biznesową, sprawdzać czy pewne zasady nie zostały złamane. Ale niekoniecznie jest to praca dla agregatu. W DDD jest jeszcze coś takiego jak serwisy domenowe, i tam bym raczej widział wykonywanie logiki którą opisałeś.

Tutaj jest kilka wątków na tematy ogólnie związane z DDD i agregatami. Mam nadzieję że niektóre moje wypowiedzi rozjaśnią dodatkowo pewne kwestie. Jeśli której posty Ci się podobają/pomagają do daj lajka :)

DDD - optymalizacja operacji na dużych kolekcjach wchodzących w skład agregatu i paginacja
Event sourcing, CQRS, a do tego message brokers
CQRS - obsługa kasowania agregatu.

1

@Aventus:
Te granice transakcyjnosci agregatu to takie dość abstrakcyjne pojęcie. Może lepiej mi będzie to zrozumieć jak zobacze przykład gdzie takie granice są łamane. Czy to jest związane z zasadą, że w 1 transakcji powinno się zapisać tylko 1 agregat?

Co do przykładu z Comparision.
No ok, zrobisz serwis domenowy ale nadal musisz zapisać ten ranking dla obu ciuchów. Musisz coś wyciągnąć z Repozytorium, zmienić i zapisać jakby nie było. Jak to widzisz inaczej ?

3
Bambo napisał(a):

@Aventus:

Te granice transakcyjnosci agregatu to takie dość abstrakcyjne pojęcie. Może lepiej mi będzie to zrozumieć jak zobacze przykład gdzie takie granice są łamane. Czy to jest związane z zasadą, że w 1 transakcji powinno się zapisać tylko 1 agregat?

Ogólna zasada jest właśnie taka że na jedną operację modyfikuje się jeden agregat. Zwróć jednak uwagę że nie chodzi tu o zapisywanie, bo to oddzielna kwestia. Rzecz w tym żeby dany proces mógł zostać obsłużony przez agregat który może dokonać decyzji/walidacji zasad biznesowych na podstawie danych które posiada, bez niczego z zewnątrz co by mogło na to wpłynąć lub nagle się zmienić. Dam Ci przykład z życia wzięty kiedy pracowałem przy systemie ubezpieczeniowym. Chodziło o wyszukiwanie, kupno i zarządzanie polisami- a więc również dokonywanie zmian polisy (zmiana adresu, sytuacji finansowej itp.), odnawianie polis, anulowanie itp. Czy w systemie mieliśmy jakiś agregat który nazywał się "polisa"? Nie. Istniało coś takiego jak ID polisy aby móc powiązać ze sobą operacje na konkretnej polisie, ale agregaty jakie mieliśmy to np. NewPolicy, PolicyRenewal, MidTermAdjustment (to domenowe określenie modyfikacji polisy) itp.

Co do przykładu z Comparision.
No ok, zrobisz serwis domenowy ale nadal musisz zapisać ten ranking dla obu ciuchów. Musisz coś wyciągnąć z Repozytorium, zmienić i zapisać jakby nie było. Jak to widzisz inaczej ?

No tak, musisz to zapisać. Nie widzę tu żadnego problemu. Prawdopodobnie Twoją główną przeszkodą do zrozumienia zagadnienia jest nadmierne skupianie się na zapisywaniu. Zapisywanie to natomiast sprawa drugorzędna.

1

@Aventus:
Ale jaki jest sens modyfikacji agregatu bez jego zapisywania? Zmieniamy stan naszego systemu i na końcu zawsze chcemy to spersystować jeśli zmiana się powiodła.

Dlatego nie bardzo potrafię sobie wyobrazić jak chcesz zmienić i utrwalić ranking dwóch ciuchów beż użycia jakiegoś agregatu. Przecież to agregaty się zapisuje poprzez repozytoria.

Ok, agregat powinien być samodzielnie w stanie przeprowadzić operacje beż info z zewnątrz, ale co jeśli do przeprowadzenia operacji musisz o coś odpytać inny mikroserwis albo usługę np Amazona? Załóżmy, że musisz sprawdzić czy zdjęcie nie jest wulgarne i korzystasz z jakiegoś AI z AWS .. albo sprawdzasz kursy euro/pln. To tak już nie można robić ?

Przykłady z tym NewPolicy bardzo fajne. Może właśnie tak mógłbym porozdzielać mój Item na różne takie mniejsze agregaciki?

3
Bambo napisał(a):

Ale jaki jest sens modyfikacji agregatu bez jego zapisywania? Zmieniamy stan naszego systemu i na końcu zawsze chcemy to spersystować jeśli zmiana się powiodła.

Nie zrozumiałeś mnie. Oczywiście że agregat się zapisuje. Rzecz w tym że Ty się na tym skupiasz, a nie o to w tym wszystkim chodzi. Fajnie to swoją drogą obrazuje gdzie często leży największy problem przy nauce DDD, skąd biorą się niedopowiedzenia itp. DDD to zmiana sposobu myślenia, można by rzec że wręcz zmiana paradygmatu. Chodzi o to żeby w pierwszej kolejności skupić się na procesie jaki dany agregat obsługuje, zamiast na perzystencji. Przy dobrze zamodelowanym agregacie wątpliwości w kwestii zapisywania agregatu się nie pojawią.

Dlatego nie bardzo potrafię sobie wyobrazić jak chcesz zmienić i utrwalić ranking dwóch ciuchów beż użycia jakiegoś agregatu. Przecież to agregaty się zapisuje poprzez repozytoria.

Ale nikt nie mówi że wynik operacji musi być pełnoprawnym agregatem. Może być zapisanym widokiem. I tak, możesz do tego nawet użyć dedykowanego repozytorium. Świat się od tego nie zawali.

Ok, agregat powinien być samodzielnie w stanie przeprowadzić operacje beż info z zewnątrz, ale co jeśli do przeprowadzenia operacji musisz o coś odpytać inny mikroserwis albo usługę np Amazona? Załóżmy, że musisz sprawdzić czy zdjęcie nie jest wulgarne i korzystasz z jakiegoś AI z AWS .. albo sprawdzasz kursy euro/pln. To tak już nie można robić?

To już nie leży w gestii agregatu, bo łamiesz tym zasadę transakcyjności. Taka walidacja bardziej nadaje się dla serwisu domenowego. Ewentualnie możesz mieć tę logikę w agregacie, ale musisz mu wcześniej dostarczyć potrzebnych danych do walidacji. Innym dobrym przykładem jest fikcyjny agregat User oraz zasada że każdy użytkownik musi mieć unikalny adres e-mail. Skąd agregat User ma wiedzieć czy może zezwolić na utworzenie nowego użytkownika z mail "[email protected]"? Nie może tego wiedzieć, bo użytkownik (konkretna instancja tego agregatu) jest jednym użytkownikiem, a nie wszystkimi użytkownikami w systemie. Tutaj fajny artykuł na ten temat. Może trochę rozjaśni.

1

@Aventus: kojarzę ten post, dzięki. Wiem, że unikalność emaila czy tam profanity zdjęcia to nie sprawa agregatu. Czyli jak rozumiem powinniśmy mieć jakiś serwis domenowy gdzie mamy sobie port, którym odpytamy o unikalność maila/poprawność zdj itd, dopiero przetworzymy agregat i na końcu go zapiszemy. Czyli standardowy flow jak w apce z tutorialu.

A co do mojego przykładu.. no dobra, zrobimy do tego serwis domenowy, zaciągnie on rankingi obu sukienek, przeliczy, zwróci nowe rankingi a potem dedykowane repo zapisze. Wszystko fajnie, ale w sumie to można tak robić z każdą operacja i po co nam agregaty? Robimy przez to anemiczny kod.. poza tym wynik operacji takiego domenowego serwisu o jakim mowisz czym jest? Value Objectem zapewne. Nie widziałem nigdzie repo od VO. Dlaczego nie można ulepić z tego małego agregatu tak jak ja zaproponowałem?

2

Ja o agregatach bardziej myśle w kontekście warunków, które musza być zawsze spełnione, w związku z czym trzeba z bazy pobierać minimalny zestaw danych, który to zapewni. Jakieś przeliczanie rankingów czy sukienek może nie być problemem adekwatnym do DDD. Sprawdzanie unikalności pól chyba też średnio przystaje - albo trzeba to zrzucić na bazie, albo zapewnić atomowość operacji poprzez implementacje wzorca Availability czy to domain servicem czy agregatem (na jedno wyjdzie).

1

@Bambo: mi się wydaje że na siłę próbujesz upchać ten konkretny przypadek w DDD, a konkretnie w agregaty. Przecież sam napisałeś wczesniej że zapisujesz tylko wynik obliczenia w tym przypadku, a więc jak wcześniej wspomniałem jest to pewien model widoku a nie element jakiejś konkretnej transakcji biznesowej.

Oczywiście że w DDD należy unikać anemicznego modelu, i w wyborze między agregatem a serwisem domenowym należy starać się używać tego pierwszego kiedy tyko to możliwe i ma sens.

Nie dajmy się zwariować. Dogmaty są potrzebne ale trzymanie się ich w 100% to wiązanie sobie rąk i stwarzanie samemu sobie problemów tam gdzie można zastosować znacznie prostsze rozwiązanie.

0

No ok, może to jest prosty case i nie trzeba tu zaprzęgać DDD, ale analizuję to w kategoriach szkoleniowych.

Moja propozycja agregatu Comparision chyba spełnia wszystkie reguły? Ładuję minimalną ilość danych, spełniam granicę transakcyjnośći. Na końcu mogę nawet event opublikować. Mam logikę ładnie zamkniętą wewnątrz agregatu. Ok może oram temat, ale po prostu staram się zrozumieć dlaczego źle to zrobiłem.

1

Moim zdaniem tutaj po prostu nie ma miejsca dla zaangażowania agregatu, bo nie ma żadnego złożonego procesu biznesowego. Jeśli tylko zapisanie preferencji użytkownika (operacja CRUD) oraz przeliczenie rankingu/preferencji względem innych preferencji (jakiś oddzielny proces, najlepiej w tle).

3

Czyli reasumując jest to miejsce dla użycia zwykłego cruda, gdzie wyciągnę dwa obiekty Cloth, przeliczę i zapisze?

Ok, a macie przykład Agregatu że złożonym procesem biznesowym? Cos więcej niż if na statusie + prosta operacja ? ;p

@Aventus
Tak na szybko czytałem sobie posty, które podlinkowałeś wyżej, dam lajki później jak będę sobie czytał na spokojnie, bo wydawało się sensowne jak zawsze, ale zaczęła mnie zastanawiać jedna rzecz jeśli chodzi o eventy - w sumie już mnie zastanawia od dłuższego czasu.
Czy event emitowany przez agregat powinien być mały i przedstawiać tylko to co się zmieniło? Czy nie ma problemu aby zawierał wszystkie atrybuty agregatu?
No bo o co mi chodzi ... załóżmy, że mamy mikrowserwis ItemService z agregatem Item i po updacie ceny emituje on zdarzenie ItemPriceUpdated. Z jednej strony powinniśmy do payloadu zdarzenia dać itemId i nową cenę. ALE ..

Co jeśli inne mikroserwisy trzymają sobie u siebie kopie itemów - okrojone bądź nie? Albo mamy CQRS i musimy jakoś odświeżać nasze read modele?
Taki okrojony event nic nie da. CQRS albo inne mikroserwisy chciałyby nasłuchać na event, który zawiera WSZYSTKIE pola z agregatu item -> żeby móc sobie odświeżyć wsio co potrzebują. Czy to jest jakiś inny rodzaj eventów niż domenowe i powinno być emitowane w ogóle w innym miejscu czy jak to wygląda?

Jeszcze sprawa zduplikowanych/zagubionych eventów - przykład z realnego problemu w pracy. Pojawił się problem zduplikowanych eventów, albo takich, które przychodziły w złej kolejności. Chcieliśmy zatem sprawić, aby mikroserwisy stały się idempotentne. Robimy tak, że agregat przy wykonaniu swojej logiki zaraz przed wyemitowaniem zdarzenia podbija swoją wersję i też przekazuje ją w payloadzie eventu. Każdy inny mirkoserwis reagujący na event sprawdza czy lokalna kopia jest mniejsza niż to co przyszło - jeśli tak to przetwarza event, jeśli nie to odrzuca. Czy to jest wg Ciebie ok?

A co z eventami, które w jakiś niewyjaśnionych okolicznościach gdzieś totalnie znikną ? :D Czy to jest możliwe w ogóle? Jakie powinny być wtedy procedury? Zapewne takie coś wyychodzi dopiero jak user zgłosi sprawę do supportu ;p

2
Bambo napisał(a):

Czyli reasumując jest to miejsce dla użycia zwykłego cruda, gdzie wyciągnę dwa obiekty Cloth, przeliczę i zapisze?

Moim zdaniem tak.

Ok, a macie przykład Agregatu że złożonym procesem biznesowym? Cos więcej niż if na statusie + prosta operacja ? ;p

Musiałbym poszukać. Było trochę przykładów, zarówno od MS jak i osób prywatnych. Wiadomo, takie przykłady miały również głosy krytyki bo nigdy nie będzie tak że dla wszystkich coś będzie zrobione prawidłowo ;)

Czy event emitowany przez agregat powinien być mały i przedstawiać tylko to co się zmieniło? Czy nie ma problemu aby zawierał wszystkie atrybuty agregatu?

Zazwyczaj taki event zawiera tylko to co się zmieniło.

No bo o co mi chodzi ... załóżmy, że mamy mikrowserwis ItemService z agregatem Item i po updacie ceny emituje on zdarzenie ItemPriceUpdated. Z jednej strony powinniśmy do payloadu zdarzenia dać itemId i nową cenę. ALE ..

Co jeśli inne mikroserwisy trzymają sobie u siebie kopie itemów - okrojone bądź nie? Albo mamy CQRS i musimy jakoś odświeżać nasze read modele?
Taki okrojony event nic nie da. CQRS albo inne mikroserwisy chciałyby nasłuchać na event, który zawiera WSZYSTKIE pola z agregatu item -> żeby móc sobie odświeżyć wsio co potrzebują. Czy to jest jakiś inny rodzaj eventów niż domenowe i powinno być emitowane w ogóle w innym miejscu czy jak to wygląda?

Przecież jeśli jakiś serwis trzyma swoja kopię itemów, to przy zmianie ceny wystarczy że zaktualizuje cenę istniejącego itemu po jego ID. Skoro cena zostałą zmieniona, to taki item musiał wcześniej być utworzony, a do tego powinien zostać wyemitowany event, np. ItemAddedToWarehouse. Wtedy po stronie innego serwisu obsługa eventów wygląda tak:

  • Event ItemAddedToWarehouse odebrany
  • Item utworzony w lokalnej bazie serwisu
  • Event ItemPriceUpdated odebrany
  • Item zaktualizowany w lokalnej bazie serwisu z nową ceną

Jeszcze sprawa zduplikowanych/zagubionych eventów - przykład z realnego problemu w pracy. Pojawił się problem zduplikowanych eventów, albo takich, które przychodziły w złej kolejności. Chcieliśmy zatem sprawić, aby mikroserwisy stały się idempotentne. Robimy tak, że agregat przy wykonaniu swojej logiki zaraz przed wyemitowaniem zdarzenia podbija swoją wersję i też przekazuje ją w payloadzie eventu. Każdy inny mirkoserwis reagujący na event sprawdza czy lokalna kopia jest mniejsza niż to co przyszło - jeśli tak to przetwarza event, jeśli nie to odrzuca. Czy to jest wg Ciebie ok?

De-duplikacja eventów to w ogóle oddzielny temat. Są do tego różne podejścia. Ja najczęściej spotykałem się z jakąś kolekcją ID przetworzonych eventów. Każde takie ID jest trzymane tylko przez jakiś czas, bo zakłada się że event nie będzie dostarczony później niż N dni etc.

A co z eventami, które w jakiś niewyjaśnionych okolicznościach gdzieś totalnie znikną ? :D Czy to jest możliwe w ogóle? Jakie powinny być wtedy procedury? Zapewne takie coś wyychodzi dopiero jak user zgłosi sprawę do supportu ;p

No jest taka możliwość, i wtedy właśnie masz błąd który jest zgłaszany. My programiści zawsze chcemy zapobiec wszelkim możliwym sytuacjom, a często jest tak że biznes sam powie że w takiej sytuacji skierowanie użytkownika do supportu to nie koniec świata. ALE dokładnie do tego problemu który opisujesz został wymyślony event sourcing- wtedy event zawsze jest zapisywany transaksyjnie, i logika biznesowa nie wykonuje nic więcej jeśli zapisanie eventów się nie powiodło. Event store w takiej sytuacji staje się "jedynym źródłem prawdy". To trochę tak jak z zapisywaniem rekordu do bazy danych- jeśli się nie powiódł to mamy błąd krytyczny.

0

@Aventus:
A to jest zawsze tak, że 1 bounded context = 1 agregat?
Bo tak to jest zazwyczaj przedstawiane. I czy 1 bounded context = 1 mikroserwis?

A widzisz, to co napisałeś z rozmiarem payloadu eventu ma sens. U nas architekt kazał do każdego eventu wrzucić WSZYSTKIE atrybuty agregatu, żeby przy odebraniu go przez inny mikroserwis od razu ten mikroserwis updatował sobie wszystkie pola - tak defensywnie, w razie gdyby jakieś stare eventy się zgubiły i coś było niespójne ;p

2

A to jest zawsze tak, że 1 bounded context = 1 agregat?

Oczywiście że nie, to by niezły chaos wprowadziło :o Każdy bounded context może mieć wiele agregatów. Chodzi bardziej o ich logiczną spójność, czyli aby nie mieć agregatów które naturalnie nie należą do danego kontekstu.

Bo tak to jest zazwyczaj przedstawiane. I czy 1 bounded context = 1 mikroserwis?

Jeśli stosuje się mikroserwisy i DDD to najczęściej tak się robi. Ale nie jest to jakaś sztywna reguła.

A widzisz, to co napisałeś z rozmiarem payloadu eventu ma sens. U nas architekt kazał do każdego eventu wrzucić WSZYSTKIE atrybuty agregatu, żeby przy odebraniu go przez inny mikroserwis od razu ten mikroserwis updatował sobie wszystkie pola - tak defensywnie, w razie gdyby jakieś stare eventy się zgubiły i coś było niespójne ;p

Takie duże eventy to zazwyczaj eventy integracyjne, i używa się ich częściej do komunikacji między systemami, czyli np. jakiś serwis poza domeną.

0

@Aventus: ok, a w takim razie można dzielić value objecty między modułami/pakietami w jednym BC?

Załóżmy, że masz jakiś BC i masz sobie agregat Customer, który zawiera takie VO jak CustomerId oraz możesz z niego zrzucić VO jak CustomerSnapshot.
Czy jeśli teraz masz w tym BC inny pakiet/moduł, załóżmy standardowo z zamówieniem to możesz użyć powyższych VO?

Czy jednak odesparowujesz pakiety/moduły poprzez jakieś dtosy? Trochę mi się już to wydaje na siłę i przeinżynierowane bo będziemy mapować w tą i w drugą obiekty 1:1.
Tak jak agregatów, fabryk, repozytoriów czy serwisów domenowych nie dzieliłbym między modułami tak takie value objecty wydają się naturalne, żeby móc je uzyć w innym miejscu - ale w obrębie jednego BC, bo gdzie indziej taki VO może zmienić znaczenie.

1

To trochę zły przykłady VO moim zdaniem. VO to np. OrderLineItem, OrderAddress itp. Wszystkie mogą być właściwościami roota Order. A czy je używać w innych agregatach w tym samym BC? Jeśli to ma sens to nie widzę przeszkód. CustomerSnapshot na pewno nie uznawał bym za VO ani nie używał bezpośrednio w agregacie jako część jego transakcyjnych właściwości. Jeśli dobrze rozumiem intencję, to snapshot służy to odbudowania agregatów, np. jeśli masz event sourcing i dużo eventów.

Czy jednak odesparowujesz pakiety/moduły poprzez jakieś dtosy? Trochę mi się już to wydaje na siłę i przeinżynierowane bo będziemy mapować w tą i w drugą obiekty 1:1.

No jeśli miałbym coś używać w warstwie niżej, np. w warstwie aplikacji (czyli Web API) to zdecydowanie bym to odseparował. Tym bardziej że często DTO będzie miało inny kształt niż agregat.

1

@Aventus
Snapshot to miałem na myśli struktura danych przedstawiają agregat - aby móc go spersystowac. No bo tak to mamy wszystko schowane, nie wystawiamy getterow.
No a CustomerId nie jest przykładem VO?

Trochę się też opierałem na tym

Warstwa aplikacji to web api? Z tego co wiem warstwa aplikacji to cienka warstwa między właśnie warstwa prezentacji (web api) a domena - czyli orkiestruje domenę, zarządza transakcjami i security. Tu też możemy np sprawdzić unikalność maila itd. Czy to nie tak?

3
Bambo napisał(a):

Snapshot to miałem na myśli struktura danych przedstawiają agregat - aby móc go spersystowac. No bo tak to mamy wszystko schowane, nie wystawiamy getterow.

Nie do końca widzę sens takiego podejścia, ale rozumiem że to kwestia preferencji. Czy wystawisz gettery z agregatu, czy opakujesz stan agregatu w jakiś spanshot, to i tak... wystawiasz stan agregatu. Znów, jeśli to ma związek z perzystencją to żaden to VO. VO to coś co jest związanego z domenową naturą agregatu, i tyle.

No a CustomerId nie jest przykładem VO?

CustomerId to jest po prostu wrapper na jakiś tam prymitywny typ, aby uniknąć primitives obsession. Sam nie do końca się zgadzam z tym podejściem no ale nie o tym temat. Nie nazywałbym go VO w takim samym sensie co np. VO przedstawiający order item. Tak jak wspomniałem, jest to zwykły wrapper. Dla mnie VO to coś bardziej złożonego niż jeden string albo Guid opakowy w jakąś klasę/strukturę.

Warstwa aplikacji to web api? Z tego co wiem warstwa aplikacji to cienka warstwa między właśnie warstwa prezentacji (web api) a domena - czyli orkiestruje domenę, zarządza transakcjami i security. Tu też możemy np sprawdzić unikalność maila itd. Czy to nie tak?

Nie, unikalność maila to już warstwa domeny, a konkretnie serwis domenowy. Natomiast warstwa aplikacja to tak jak wspomniałem, ale ja widzę to jako integralna część np. projekt web API. Ale to już moja osobista dygresja.

2

@Bambo myślę, że na siłę chcesz wstawić jakieś koncepty DDD zamiast zadać sobie pytanie, jaki problem chcesz rozwiązać… zrób sobie jedną jebitną encję, nazwij ją sobie jak musisz agregatem i może być po sprawie :)

2

Też tak sądzę. DDD zostało zdefiniowane po to żeby rozwiązywać złożone problemy biznesowe, a nie tworzyć złożone problemy z prostych.

0

@Charles_Ray: @Aventus

Zgadzam się, że rozmawiamy tutaj o przeinżynierowanu ;p Ja po prostu staram się wyłapać rzeczy, które mogę robić totalnie źle - oczywiście poza tym, że teraz przeinżynierowywuję, a to jest złe, wiem - ale robię to w celach szkoleniowych i badawczych ;p

Nie do końca widzę sens takiego podejścia, ale rozumiem że to kwestia preferencji. Czy wystawisz gettery z agregatu, czy opakujesz stan agregatu w jakiś spanshot, to i tak... wystawiasz stan agregatu. Znów, jeśli to ma związek z perzystencją to żaden to VO. VO to coś co jest związanego z domenową naturą agregatu, i tyle.

Zauważ, że gettery będą Ci zwracać bezpośrednio bebechy, czyli encje i vo, a taki snapshot to spłaszczona struktura danych składająca się z prymitywów (trochę zdj rentgenowskie). W jakiś sposób muszę taki agregat zserializować do bazy albo potem utworzyć z niego event do opublikowania. A zwolennikiem getterów po prostu nie jestem.

CustomerId to jest po prostu wrapper na jakiś tam prymitywny typ, aby uniknąć primitives obsession. Sam nie do końca się zgadzam z tym podejściem no ale nie o tym temat. Nie nazywałbym go VO w takim samym sensie co np. VO przedstawiający order item. Tak jak wspomniałem, jest to zwykły wrapper. Dla mnie VO to coś bardziej złożonego niż jeden string albo Guid opakowy w jakąś klasę/strukturę.

Czyli wolisz używać prymitywów zamiast obiektów? Ja akurat jestem zwolennikiem silnego typowania, bo i od razu masz zwalidowane dane wejściowe, a poza tym sygnatury metody jawnie Ci mówi co jest po kolei w argumentach i trudniej sie pomylić. Też łatwiej ze sobą składać operacje. Jak zwracam z metody listę, która nie może być pusta (wymagania biznesowe) to zwracam obiekt NonEmptyTasksList, która w środku już ma logikę sprawdzania długości. Potem w kodzie dzięki temu mam pewność, że pracuję z bezpiecznym typem - tu listą.

Rozumiem też o jakie VO Ci chodzi - takie typowo związane z agregatem, nie wychodzące poza jego obręb pewnie. Jest jakiś klocek DDD, który nazywa moje "wrappery na typy prymitywne"? Chociaż z drugiej strony w 80% przypadkach właśnie takie CustomerId czy EmailAddress jest określane mianem VO.

Nie, unikalność maila to już warstwa domeny, a konkretnie serwis domenowy. Natomiast warstwa aplikacja to tak jak wspomniałem, ale ja widzę to jako integralna część np. projekt web API. Ale to już moja osobista dygresja.

No właśnie .. na tym blogu co mi wysłałeś z tym artem o unikalności emaila autor mówi, że to może być albo serwis aplikacyjny albo serwis domenowy - w zależności od tego czy masz potrzebę wydzielenia tego serwisu domenowego z racji tego, że puchnie Ci serwis aplikacyjny.

Dla mnie serwis aplikacyjny to taka cienką warstwa, która oddziela infrastrukturę od domeny - mapuje dto na obiekty domenowe, zarządza security i transakcjami, orkiestruje logikę.
I nie do końca czuję scalenia tego z warstwą webową czyli REST kontrolerami.

Jak dla mnie serwis aplikacyjny powinien być tak stworzony, aby móc go użyc wystawiając REST, ale również z jakimś wbudowanym GUI czy nawet konsolą. Czyli to taka fasada do Twojego jądra systemu.
Zastanawiało mnie jedynie, czy jak mamy sobie metodę w serwisie aplikacyjnym np. do rejestrowania userów to czy powinna ona jako argumenty przyjmować prymitywy czy już typy - np Email.
Z prymitywami ten plus, że mamy mocniej odesparowane warstwy, bo infra (UI, REST) nic nie wie o czymś takim jak Email. Z drugiej strony musimy z emaila mapować na stringa i odwrotnie - jeśli np serwisy aplikacyjne komunikują się ze sobą. W Vernonie przeczytałem, że tu akurat zależy od przypadku, a tak czy siak reguły kierunku zależności nie łamię, bo nadal zależności idą od zewnątrz do środka.

2

@Bambo:

Bambo napisał(a):

Zauważ, że gettery będą Ci zwracać bezpośrednio bebechy, czyli encje i vo, a taki snapshot to spłaszczona struktura danych składająca się z prymitywów (trochę zdj rentgenowskie). W jakiś sposób muszę taki agregat zserializować do bazy albo potem utworzyć z niego event do opublikowania. A zwolennikiem getterów po prostu nie jestem.

Nie za bardzo rozumiem co masz na myśli pisząc o spłaszczonej strukturze. Możesz to jakoś zobrazować? Przyjmijmy taki agregat:

class Order
{
  private List<OrderItem> OrderItems { get; }
  private List<OrderDiscount> Discounts { get; }
  private Address DeliveryAddress { get; } 
}

OrderItem, OrderDiscount oraz Address to są VOs. Jak chcesz spłaszczyć ten stan agregatu?

Czyli wolisz używać prymitywów zamiast obiektów? Ja akurat jestem zwolennikiem silnego typowania, bo i od razu masz zwalidowane dane wejściowe, a poza tym sygnatury metody jawnie Ci mówi co jest po kolei w argumentach i trudniej sie pomylić. Też łatwiej ze sobą składać operacje. Jak zwracam z metody listę, która nie może być pusta (wymagania biznesowe) to zwracam obiekt NonEmptyTasksList, która w środku już ma logikę sprawdzania długości. Potem w kodzie dzięki temu mam pewność, że pracuję z bezpiecznym typem - tu listą.

Jestem w stanie zrozumieć użycie NonEmptyTasksList bo to trochę tak jak z Optional<T> czy coś podobnego. Odnosiłem się bardziej do typów opakujących typy prymitywne takie jak string czy Guid. Szczególnie jeśli dla każdego rodzaju ID tworzy się oddzielny typ, ale każdy jest tylko wrapperem na string, czyli np. CustomerId, OrderId itp. Już prędzej jestem w stanie zrozumieć jakiś ogólny typ UniqueIdentifier którego następnie wszędzie używamy. Szczególnie jeśli zastosuje się .Net 5 i recordy, wtedy mam value comparison bez pisania dodatkowego kodu. Ale jak już pisałem jest to tylko moje zdanie. Uważam to za przerost formy nad treścią, i zazwyczaj nie mam potrzeby opakowywać stringa czy Guida które przedstawiają jakieś ID.

Rozumiem też o jakie VO Ci chodzi - takie typowo związane z agregatem, nie wychodzące poza jego obręb pewnie. Jest jakiś klocek DDD, który nazywa moje "wrappery na typy prymitywne"? Chociaż z drugiej strony w 80% przypadkach właśnie takie CustomerId czy EmailAddress jest określane mianem VO.

Tak, o takie VO mi chodziło.

No właśnie .. na tym blogu co mi wysłałeś z tym artem o unikalności emaila autor mówi, że to może być albo serwis aplikacyjny albo serwis domenowy - w zależności od tego czy masz potrzebę wydzielenia tego serwisu domenowego z racji tego, że puchnie Ci serwis aplikacyjny.

Z tym akurat się nie zgadzam. To czy email w danym kontekście musi być unikalny czy nie to już typowa zasada biznesowa, a więc jej miejsce jest w serwisie domenowym. Co jeśli ekspert domenowy powie Ci "Nasz zespół support ma zasadę że kilku pracowników może mieć dostęp do tego samego adresu email, ale adres email nie może być przypisany do więcej niż 5 pracowników"? Czym to jest jak nie zasadą biznesową? A umieszczanie zasad biznesowych w serwisach aplikacji to już proszenie się o kłopoty, bo nie dość że masz niektóre zasady biznesowe w serwisach domenowych zamiast w agregatach (co akurat czasem jest konieczne jak już wyjaśniliśmy) to jeszcze część z tych zasad umieszczasz w całkowicie oddzielnej warstwie.

Dla mnie serwis aplikacyjny to taka cienką warstwa, która oddziela infrastrukturę od domeny - mapuje dto na obiekty domenowe, zarządza security i transakcjami, orkiestruje logikę.
I nie do końca czuję scalenia tego z warstwą webową czyli REST kontrolerami.

Jak dla mnie serwis aplikacyjny powinien być tak stworzony, aby móc go użyc wystawiając REST, ale również z jakimś wbudowanym GUI czy nawet konsolą. Czyli to taka fasada do Twojego jądra systemu.

Rozumiem, ale moim zdaniem to sztuka dla sztuki. Koniec końców jeśli będziesz pisał system oparty o serwisy webowe- a szczególnie mikroserwisy- to i tak wszystkie kwestie związane z uwierzytelnianiem, walidacją DTO itp. będą sprawdzane na wejściu. A "na wejściu" oznacza tak naprawdę w warstwie "aplikacji", co ja rozumiem właśnie przez web API. W takim przypadku argument o potencjalnym UI desktopowym nie ma większego znaczenia bo jakiego klienta byś nie użył to będzie pod spodem uderzał po HTTP. A próbowanie nadmiernego wyabstrahowania warstwy aplikacji w imię "a co jeśli..." to moim zdaniem tak jak powiedziałem sztuka dla sztuki. U mnie struktura projektu webowego wygląda zazwyczaj tak że mam w nim folder Extensions gdzie mam kod mapujący, i czasem właśnie Services czyli serwisy czysto warstwy aplikacji. Jeśli używa się mikroserwisów to wszystko to jest na tyle kompaktowe że świetnie pasuje do jednego projektu (w sensie projektu .Net) i tworzy jedną, spójną całość,

Zastanawiało mnie jedynie, czy jak mamy sobie metodę w serwisie aplikacyjnym np. do rejestrowania userów to czy powinna ona jako argumenty przyjmować prymitywy czy już typy - np Email.
Z prymitywami ten plus, że mamy mocniej odesparowane warstwy, bo infra (UI, REST) nic nie wie o czymś takim jak Email. Z drugiej strony musimy z emaila mapować na stringa i odwrotnie - jeśli np serwisy aplikacyjne komunikują się ze sobą. W Vernonie przeczytałem, że tu akurat zależy od przypadku, a tak czy siak reguły kierunku zależności nie łamię, bo nadal zależności idą od zewnątrz do środka.

Ano widzisz, mając warstwę aplikacji w zrozumieniu takim jak podałem wyżej taki dylemat nie występuje. Endpointy przyjmują DTO, a zanim to trafi wyżej do warstwy domenowej to musi zostać zmapowane na odpowiednie obiekty domenowe. Rzecz w tym że to mapowanie odbywa się w tej samej warstwie, zamiast "sztucznie" pchać to przez jakąś warstwę abstrakcji. Po co komplikować coś co może być uproszczone, zachowując przy tym spójną i logiczną całość oraz separację?

To wszystko można podsumować do dwóch dobrze znanych zasad clean code- You Ain't Gonna Need It oraz Keep It Simple Stupid.

1

@Aventus:

Ja generalnie listy i inne kolekcje opakowuję jeszcze w obiekty. Chyba nawet to taka zasada z dobrych praktyk. Ale do rzeczy ..
załóżmy, że u Ciebie te listy są mutowalne, a OrderDiscount to po prostu wrapper na inta

to ja bym zrobił coś takiego:

OrderSnapshot snapshot() {
   return OrderSnapshot.builder()
     .items(new ArrayList<>(orderItems))
     .discounts(discounts.map(it -> it.getDiscount().toList()))
     .city(deliveryAddress.getCity())
     .street(deliveryAddress.getStreet())
     .build()
}

Po prostu spłaczam strukturę, żeby było jak najmniej zagnieżdzeń (bo w agregacie masz VO, które mogą zawierać inne VO itd). I dodatkowo to co ma sens to zmieniam na prymitywy. Jakbym miał VO Name to bym spłaszczył go do stringa. Dlatego jestem zwolennikiem takich "rzutów rentgenowskich" niż getterów. Też wiesz .. ktoś da gettera na mutowalną encję lub jakąś kolekcję i tragedia gotowa.

Z tym akurat się nie zgadzam. To czy email w danym kontekście musi być unikalny czy nie to już typowa zasada biznesowa, a więc jej miejsce jest w serwisie domenowym. Co jeśli ekspert domenowy powie Ci "Nasz zespół support ma zasadę że kilku pracowników może mieć dostęp do tego samego adresu email, ale adres email nie może być przypisany do więcej niż 5 pracowników"? Czym to jest jak nie zasadą biznesową? A umieszczanie zasad biznesowych w serwisach aplikacji to już proszenie się o kłopoty, bo nie dość że masz niektóre zasady biznesowe w serwisach domenowych zamiast w agregatach (co akurat czasem jest konieczne jak już wyjaśniliśmy) to jeszcze część z tych zasad umieszczasz w całkowicie oddzielnej warstwie.

No ja też jestem zwolennikiem tego, żeby warstwa serwisów aplikacyjnych była jak najcieńsza. A u Ciebie te serwisy aplikacyjne też czasami ze sobą gadają? Czy są one w pełni autonomiczne? No bo załóżmy masz w BC kilka agregatów i pewnie każdy z nich ma swój jakiś moduł/pakiet. To robisz tak, że na 1 agregat jest 1 serwis aplikacyjny (i pewnie kilka domenowych) czy jakoś inaczej to organizujesz?

W sensie chodzi mi o to, że masz np agregat Customer oraz Product no i jeśli są one między sobą powiązane to przez ID. Ale czy teraz masz tak, że jest u Ciebie CustomerApplicationService oraz ProductApplicationService? Jeśli tak to czy dopuszczasz, żeby gadały one ze sobą jeśli potrzeba?

Rozumiem, ale moim zdaniem to sztuka dla sztuki. Koniec końców jeśli będziesz pisał system oparty o serwisy webowe- a szczególnie mikroserwisy- to i tak wszystkie kwestie związane z uwierzytelnianiem, walidacją DTO itp. będą sprawdzane na wejściu. A "na wejściu" oznacza tak naprawdę w warstwie "aplikacji", co ja rozumiem właśnie przez web API. W takim przypadku argument o potencjalnym UI desktopowym nie ma większego znaczenia bo jakiego klienta byś nie użył to będzie pod spodem uderzał po HTTP. A próbowanie nadmiernego wyabstrahowania warstwy aplikacji w imię "a co jeśli..." to moim zdaniem tak jak powiedziałem sztuka dla sztuki. U mnie struktura projektu webowego wygląda zazwyczaj tak że mam w nim folder Extensions gdzie mam kod mapujący, i czasem właśnie Services czyli serwisy czysto warstwy aplikacji. Jeśli używa się mikroserwisów to wszystko to jest na tyle kompaktowe że świetnie pasuje do jednego projektu (w sensie projektu .Net) i tworzy jedną, spójną całość,

To wszystko zależy gdzie wsadzasz walidację formatu i struktury (bo wiadomo, że trzeci stopień walidacja czyli biznesowa jest w domenie). Generalnie walidację formatu robi w javie framework (Spring), bo jak jest coś nie tak z JSONem to się wywali na samym wejściu requestu http.

Dalej masz walidację struktury i znowu możesz ograć to adnotacjami typu @Notnull, @MaxLength i podobnymi na poziomie webowego DTO. Mimo wszystko nadal powtarzam te walidacje na objektach typu Name, Title, Description. Nadal będę za tym, że takie wrapery na stringi i inty, czy inne booleany mają sens, bo trudniej się pomylić w argumentach metod i dodatkowo widząc w kodzie wraper a nie prymityw masz pewność, że nie jest on żadnym nullem, jest zwalidowany i generalnie bezpieczny. Hmm też się zastanawiam, bo z tego co piszesz u Ciebie warstwa webowa jest również aplikacyjną. Czyli mieszasz obsługę HTTP z transakcjami tak ? Czyli w javie w Springu endpoint wyglądałby mniej więcej tak:

@Transactional
@PostMapping("/player")
long addPlayer(@RequestBody PlayerForm newPlayer) {
  //...
}
3
Bambo napisał(a):

Po prostu spłaczam strukturę, żeby było jak najmniej zagnieżdzeń (bo w agregacie masz VO, które mogą zawierać inne VO itd). I dodatkowo to co ma sens to zmieniam na prymitywy. Jakbym miał VO Name to bym spłaszczył go do stringa. Dlatego jestem zwolennikiem takich "rzutów rentgenowskich" niż getterów. Też wiesz .. ktoś da gettera na mutowalną encję lub jakąś kolekcję i tragedia gotowa.

Nie widzę sensu takiego podejścia, tak samo jak (uwaga, będzie kontrowersyjnie) nie widzę nic złego w tym jeśli agregat ma mutowalne kolekcje, nawet publiczne! Dla czego? Ponieważ agregaty to nie są obiekty które mają być gdzieś przekazywane i modyfikowane z zewnątrz. Przy przetwarzaniu polecenia domenowego agregat ma być załadowany, odpowiednia metoda na nim wywołana i na tym ma się skończyć jego cykl życia w procesie (plus zapisanie tego agregatu lub jego stanu oczywiście). Tu nie ma nigdzie miejsca na jakieś modyfikowanie dodatkowe agregatów, chyba że przepychamy go przez różne serwisy, warstwy itp. Ale to samo w sobie przeczy idei agregatów, więc w takim przypadku trzeba zmienić swoje podejście. Tak więc usilne próbowanie zabezpieczanie pól agregatu to- wybacz że się powtarzam- sztuka dla sztuki, unikanie sytuacji która nigdy nie powinna mieć miejsca, a jeśli ktoś by wpadł na taki pomysł to powinno łatwo się rzucić w oczy i natychmiast zostać wyłapane podczas code review. Poza tym jeśli ktoś naprawdę chce to może zrobić to o czym wspomniałeś, i opakować wszystkie pola w jakieś niemutowalne typy.

No ja też jestem zwolennikiem tego, żeby warstwa serwisów aplikacyjnych była jak najcieńsza. A u Ciebie te serwisy aplikacyjne też czasami ze sobą gadają? Czy są one w pełni autonomiczne? No bo załóżmy masz w BC kilka agregatów i pewnie każdy z nich ma swój jakiś moduł/pakiet. To robisz tak, że na 1 agregat jest 1 serwis aplikacyjny (i pewnie kilka domenowych) czy jakoś inaczej to organizujesz?

W sensie chodzi mi o to, że masz np agregat Customer oraz Product no i jeśli są one między sobą powiązane to przez ID. Ale czy teraz masz tak, że jest u Ciebie CustomerApplicationService oraz ProductApplicationService? Jeśli tak to czy dopuszczasz, żeby gadały one ze sobą jeśli potrzeba?

Przede wszystkim- taka wzajemna, bezpośrednia komunikacja nie powinna mieć miejsca. A więc moja odpowiedź normalnie brzmiała by nie. Natomiast uświadomiłem sobie skąd nasze niezrozumienie w tej kwestii, i to moja wina bo tego nie wyjaśniłem do razu. Widzisz, ja nie używam w ogóle serwisów aplikacyjnych w takiej postaci, tzn. serwisów do ładowania agregatów i obsługi poleceń (*commands *). Dla czego? Ponieważ mam to wyabstrahowane i schowane, zazwyczaj w jakiejś paczce (piszę w Net więc jest to Nuget, w Twoim przypadku byłby to Maven jeśli dobrze pamiętam nazwę). To działa po prostu jak wzorzec mediator- endpoint otrzymuje request, mapuje go na domenowe *command *i wysyła za pomocą właśnie jakiegoś mediatora/publishera. Cała reszta jest transparentna- mediator/publisher otrzymuje polecenia, sprawdza jaki agregat jest skonfigurowany do odbioru tego polecania, ładuje ten agregat itd. Dodam jeszcze że ja projektuje agregaty właśnie tak że odbierają one commands, więc taki agregat zamiast np. metody AddOrderItem(string orderId, decimal pricePerUnit) będzie miał metodę Receive(AddOrderItem command). Podobnie dla innych poleceń, np. Receive(RemoveOrderItem command), Receive(ChangeDeliveryAddress command). Innymi słowy, mój kod będzie wyglądał mniej więcej tak- nie będzie żadnej warstwy pomiędzy:

// Endpoint HTTP w warstwie web/aplikacji
IActionResult AddOrderItem(AddOrderItemDto request) // Jakiś tam silnie typowane DTO
{
  _publisher.Send(new AddOrderItem(request.OrderId, request.PricePerUnit));
}

// Agregat w warstwie domenowej
class Order
{
  // Inne pola i metody
  // ...

  public void Receive(AddOrderItem command)
  {
    // Logika biznesowa
  }
}

// Kod spinający to wszystko za pomocą DI w warstwie web/aplikacji
// services to kontener DI
services.SetupAggregateRoutings(config => 
  config.OnCommand<AddOrderItem >()
    .SendToAggregate<Order>()
    .CorrelatedOn(c => c.OrderId)
);

Dalej masz walidację struktury i znowu możesz ograć to adnotacjami typu @Notnull, @MaxLength i podobnymi na poziomie webowego DTO. Mimo wszystko nadal powtarzam te walidacje na objektach typu Name, Title, Description. Nadal będę za tym, że takie wrapery na stringi i inty, czy inne booleany mają sens, bo trudniej się pomylić w argumentach metod i dodatkowo widząc w kodzie wraper a nie prymityw masz pewność, że nie jest on żadnym nullem, jest zwalidowany i generalnie bezpieczny. Hmm też się zastanawiam, bo z tego co piszesz u Ciebie warstwa webowa jest również aplikacyjną. Czyli mieszasz obsługę HTTP z transakcjami tak ? Czyli w javie w Springu endpoint wyglądałby mniej więcej tak:

@Transactional
@PostMapping("/player")
long addPlayer(@RequestBody PlayerForm newPlayer) {
  //...
}

Tutaj z kolei myślę że nasze różnice wynikają z języka/frameworka jakich używamy. W .Net pytanie Czyli mieszasz obsługę HTTP z transakcjami tak ? w zasadzie nie ma sensu, bo w .Net nie ma czegoś takiego jak oznaczanie metod atrybutami takimi jak @Transactional. Jeśli używam jakiegoś unit of work pattern który na koniec przetwarzania requestu ma zapisać wszystkie zmiany, to będzie to siedziało w warstwie infrastruktury.

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