Projektowanie agregatu, cieknącą abstrakcja, podział odpowiedzialności - DDD.

0

Projektuje agregat. Nie wiem czy idę dobrze.

Do serwera przychodzi request z obiektem.
Należy sprawdzić jego stan i zapisać go lokalnie.

Mamy wiec główny cel by go zapisać, sparsowac i wysłać dalej. Niby bardzo prosta sprawa, ale gubię się z podziałem odpowiedzialności miedzy warstwami domain a application.

Id agregatu to id requestu (za każdym razem przesłany obiekt dostanie inne ID dla naszego systemu).
Następnym elementem składowym agregatu jest entity z obiektem, które przyszło w body.
Ostatnim elementem obiektu jest sciezka zapisu, która będzie znajdować się na serwerze.

Rozumiem, ze operacje typu zapis, parsowanie powinny znajdować się w warstwie application.
Jednakże regula która odpowiada za odpowiedz na pytanie „czy można go zapisać”, powinna znajdować się w agregacie - warstwie domain.

Tylko, ze do tego potrzebny będzie nam getter! a jak jest getter to cieknie nam abstrakcja z agregatu.
Nie wiem jak zrobić teraz by było „zgodnie ze sztuką.

0

Ale to chyba domena decyduje, czy wywołać zapisywanie, czy nie?

0

@somekind: Decyzja jest na podstawie stanu obiektu, można powiedzieć, ze biznes decyduje. Czyli z tego co mówisz, to w agregacie mam mieć interfejs z repozytorium?

2

Jak najbardziej. Po to są interfejsy, żeby móc je definiować i używać jednej warstwie, a ich implementacja znajdowała się w innej.

0

Okej, idac dalej.
Zakładam, ze mam bardzo rozbudowana konstrukcje sprawdzająca to czy mogę zapisać to do bazy. Np. Muszę sprawdzić w 3 systemach informacje i porównać pewne rzeczy.
Wtedy muszę decyzyjność wyprowadzić poza obiekt?
Bo to będzie wymagać wielu nowych pól składowych i w ten sposób słabnie kohezja, przez co obiekt zaczyna łamać SRP.

Taki agregat potrzebowałaby swojego własnego serwisu jako kolejnego elementu składowego. Co będzie wykonywał dla niego zadania. Wychodzi na to, ze znowu dochodzimy do prostej zasady. Tworzyc obiekty fasadowe (z metoda pakietowa, reszta prywatne), ponadto poruszac się po interfejsach.

3

Albo ja nie rozumiem, albo robisz to źle ;-). Wydaje mi się że mam za mało informacji o tym co próbujesz zrobić i jak ten system działa.
Do serwera przychodzi request z obiektem. No jeśli mówimy o programowaniu, to jest to obiekt. "Następnym elementem składowym agregatu jest entity z obiektem, które przyszło w body." Nie do końca. W body nie przesyłasz obiektów domenowych. Przesyłasz DTO. Jeśli masz architekturę "encja na twarz i pchasz" to może to nie jest w ogóle temat na DDD? DDD to nie srebrna kula ani złoty młotek. Dokłada dużo od siebie, ale w zamian pilnujesz spójności danych i procesów biznesowych. Tutaj wygląda to z opisu na bardziej problem techniczny gdzie DDD nie musi i nie powinno pewnie być używane.
Dopiero z DTO możesz tworzyć encje. Z definicji nie przesyłasz zachowania przez sieć, a dane. A obiekt w rozumienia programowania obiektowego to zachowanie i dane.
"Jednakże regula która odpowiada za odpowiedz na pytanie „czy można go zapisać”, powinna znajdować się w agregacie - warstwie domain. "
Każdy kwadrat jest prostokątem, ale nie każdy prostokąt nie jest kwadratem.
W DDD reguły biznesowe powinny znajdować się w Domenie. Ale domena to nie tylko agregaty. To też encje, value objecty i serwisy domenowe. Czy pytanie "Czy mogę zapisać" brzmi bardziej jak pytanie do aplikacji niż do czystego biznesu. Czy Ty na pewno chcesz tutaj mieć DDD? Może napisz więcej o problemie i co próbujesz osiągnąć?

"Tylko, ze do tego potrzebny będzie nam getter! a jak jest getter to cieknie nam abstrakcja z agregatu."
Ojej. No i co z tego? Gettery jeśli mają Ci uprościć kod i wyrzucić 70% kodu który byś miał bez nich są OK. Np można dzięki nim coś łatwo sprawdzić w unit testach.Problem jest wtedy, jak używasz getera do wyciągnięcia informacji z agregatu czy tam encji i trzymasz logikę która na nich pracuje gdzieś poza nimi. A ja dalej nie rozumiem do czego Ci getter potrzebny w tym scenariuszu.

4
AreQrm napisał(a):

DDD to nie srebrna kula ani złoty młotek.

Za to często jest to po prostu brązowy klocek. ;)

0

Dzięki za odniesienie.

Wydaje mi się że mam za mało informacji o tym co próbujesz zrobić i jak ten system działa.
Tak, opisze więc głębiej. Domena tego systemu może wymagać małego wprowadzenia, wiec posłuże się domeną motoryzacji.

Przesyłasz DTO. Jeśli masz architekturę "encja na twarz i pchasz" to może to nie jest w ogóle temat na DDD?

Przesyłam DTO. Na jego podstawie będzie generowany obiekt domenowy.

Tutaj wygląda to z opisu na bardziej problem techniczny gdzie DDD nie musi i nie powinno pewnie być używane.

Kwestia zapisu zależy od stanu obiektu domenowego. Można zapisać obiekt, jeżeli spełnia zasady kompatybilnosci stawiane przez biznes np dopasowanie opon do hamulców, dodatkowo nie jest w konkretnym stanie, np BLOCKED. oraz, nie był wcześniej edytowany, informacje o edycji znajdują się wewnątrz obiektu (zwykły counter).

Czy pytanie "Czy mogę zapisać" brzmi bardziej jak pytanie do aplikacji niż do czystego biznesu. Czy Ty na pewno chcesz tutaj mieć DDD? Może napisz więcej o problemie i co próbujesz osiągnąć?

Pytanie czy mogę zapisać zależy od biznesu. Czemu?
Dlatego, że klient może dokonać zapisu zmian, ale pod wieloma regułami, np opony mogą nie pasować do klockow hamulcowych, ktore wybral. Stąd agregat o nazwie car zawiera stany, które będą kompatbilne dla zapisu lub nie, To jest typowo biznesowa zasada, która powinna być widoczna.

"Problem jest wtedy, jak używasz getera do wyciągnięcia informacji z agregatu czy tam encji i trzymasz logikę która na nich pracuje gdzieś poza nimi. A ja dalej nie rozumiem do czego Ci getter potrzebny w tym scenariuszu.

Właśnie! efekt byłby taki, że pchamy logikę biznesową na zewnątrz.

if(car.getCounter() > 1 )

Wtedy cieknie abstrakcja do serwisu aplikacji.

widzę rozwiązanie w programowaniu funkcjnym.
klasa domenowa car :

class Car {
Car changeTire(Tire tire){
//logika biznesowa i nabijany counter
}
Either<NoNieUdaloSie,UdaloSieATuMaszObiekt> save(){
//logika biznesowa dla left fail z konkretna informacja, dla right obiekt zwracany .
}
}

Trzecim rozwiązaniem jest metoda dająca informacja, czy auto jest ze sobą komplatiblne, np -> czy mamy opony pasujace do hamulców. Niemniej, nie wiem jak powinno być "zgodnie ze sztuką". Getery to cieknąca abstrakcja, po zapoznaniu z ddd, boję się ich po małe dzieci po opowieściach z duchami.

1
DKN napisał(a):

Przesyłam DTO. Na jego podstawie będzie generowany obiekt domenowy.

Pytanie pierwsze - czy nie wystarczy zwalidować DTO i w ogóle nie tworzyć zepsutej encji? To by było najszybsze i najprostsze.

Właśnie! efekt byłby taki, że pchamy logikę biznesową na zewnątrz.

if(car.getCounter() > 1 )

Wtedy cieknie abstrakcja do serwisu aplikacji.

Ale nie musi to być serwis aplikacji. :) Moim zdaniem to może być serwis domenowy, który bierze encję, uruchamia na niej jakąś logikę, walidację, co tam potrzebujesz, a potem decyduje, czy zapisać czy nie. Albo np. budowaniem encji może zajmować się jakiś builder albo fabryka, który zwróci błąd o niekompatybilności zanim jeszcze błędna encja zostanie utworzona.
W ogóle nigdy nie powinno tworzyć się błędnych encji, tak więc to nie metoda save powinna nie zapisywać złej encji, co metoda changeTire nie powinna pozwalać na błędną zmianę. Ja bym zrobił po prostu metodę: Either<NiepoprawnaOponaError, Car> changeTire(Tire tire). I albo dostajemy Car, z którym możemy zrobić jeszcze coś, np. zapisać, albo błąd, który zwracamy gdzieś do góry, do użytkownika. I nie masz ani cieknącej abstrakcji, ani encji w niepoprawnym stanie.

widzę rozwiązanie w programowaniu funkcjnym.

Nie jestem funkcyjnym hipsterem, ale wydaje mi się, że nie wystarczy dać Either, żeby było funkcyjnie. ;)

No i jeszcze taka uwaga - jeśli w encji masz metodę save() to mi to raczej na Active Record wygląda, a Active Record to jeden z gorszych antywzorców łamiących SRP, i nie wiem czy w ogóle się z DDD komponuje. W końcu DDD raczej na repozytoriach niż AR do przechowywania danych się opiera.

2

Poza rozwiązaniem technicznym (o którym już @somekind napisał) widzę tutaj teraz nieco problem. W DDD powinieneś przede wszystkim rozmawiać z biznesem, ale rozmawiać jego językiem. I najlepiej jak ten jeżyk ma przełożenie na kod. Pytanie:
Czy biznes CI powiedział o zapisie? Jeśli nie to pewnie powiedział o czymś innym. Np Można dodać oponę jeśli spełnia pewne warunki. Jeśli jest kompatybilna. I Ty wtedy sobie to przekładasz, na to że wtedy aplikacja może zapisać. Jeżeli biznes mówi o tym co można zapisać a co nie, to trzeba by tu zrobić krok wstecz i wyjaśnić jaka jest reguła a nie co biznes Ci narzuca jak ma apka działać technicznie. Czasami może to wynikać z tego, że sam biznes jest techniczny co nie jest złe samo w sobie. Ale nie zaciemniaj i nie upraszczaj sobie kodu i myślenia o zasadach tam gdzie to nie potrzebne.

Tu na przykład nie powinieneś myśleć i pytać "czy mogę zapisać" ale bardziej "Czy X jest kompatybilne z Y". I tak można by nazwać metodę w samym agregacie albo serwisie domenowym. Mówiłeś wcześniej że trzeba spytać inne systemy jeszcze o coś, więc tym bardziej nie będzie to siedzieć w agregacie.
Jak bardzo mocno nie chcesz tego wyciągać z agregatu... to możesz mieć metodę np bool/Result IsCompatybleWith(Tire tire) która zwraca true/false albo bardziej złożony komunikat (albo jeśli masz event busa dostępnego w domenie to już całkiem po prostu rzucasz tam IncompatibleTireAddedEvent) i następną którą tylko wtedy wywołąsz jak 1wsza przejdzie void AddTire(Tire tire) - i tu jeśli jesteś paranoikiem w implementacji tej metody wołąsz drugi raz na początku IsCompatybleWith dla pewności że nikt CI tego nie pominie, a jeśli to rzucisz wyjątkiem.

Ale.. najważniejsze żeby aplikacja działała. Nie ma idealnego kodu i nie będzie.Tutaj masz ciekawy talk na ten temat od Evansa: "Good Design is Imperfect Design"

6

W sumie to AreQrm wyczerpał temat, ale dodam dla potwierdzenia że masz fundamentalnie złe podejście do tego co chcesz zrobić, prawdopodobnie wynikające z niezrozumienia zagadnienia. Agregat to przede wszystkim nie jest coś co przesyła się w formie requestów HTTP. Kiedy mowa o agregatach czy DDD ogólnie, to warto pamiętać o zagadnieniach commands i events- gdzie commmand to polecenie wykonania czegoś, a event to informacja o tym co już zostało wykonane. To pierwsze można dobrze zmapować na DTO (czyli właśnie payload requestów HTTP), które następnie są przekazane do agregatu (albo w postaci zmapowanych commands albo po prostu jako lista parametrów wzięta z DTO). Decyzja o tym czy coś należy zapisać (jak już AreQrm wspomniał, określenie zapisać jest zapewne błędne a mowa raczej o tym czy można dokonać jakiejś operacji biznesowej) należy do agregatu, który zawiera zasady biznesowe dla konkretnego procesu. Tutaj bardziej objaśnione czym tak naprawdę jest agregat.

Żeby Ci to bardziej zobrazować, podział na warstwy i obiekty należące do konkretnych warstw wygląda następująco:

Warstwa aplikacji
Zawiera endpointy które przyjmują/zwracają obiekty DTO. Zajmuje się decyzjami takimi jak walidacja requestów (np. czy dane DTO zawiera wymagane pole). Warstwa ta nie posiada logiki biznesowej jako takiej. "Odpalanie" procesów polega na zmapowaniu DTO na jakiś obiekt domenowy (np. w/w command) który zostanie obsłużony w warstwie domenowej. Najlepiej zastosować do tego wzorzec mediator- wtedy warstwa aplikacji nawet nie wie gdzie dane polecenie jest obsługiwane.

Warstwa domeny
Zawiera model domeny, czyli agregaty, serwisy domenowe itp. które obsługują polecenia, dokonują walidacji zasad biznesowych itp. To tutaj decydujesz czy dana operacja ma się zakończyć powodzeniem (a więc co za tym idzie czy zapisać zaktualizowany stan agregatu), czy należy ją odrzucić.

1

Dziękuje wszystkim za odpowiedzi.
Przeanalizowałem to co napisaliście i wyprostowalem duzo w głowie.

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