DRY, długie klasy i inne problemy Entity w podejściu DDD

0

Zaczynam powoli interesować się DDD więc wygląda na to, że będzie wiele pytań z mojej strony w najbliższym czasie. Zacznijmy jednak od Entity.

Do tej pory stosowałem anemiczne modele i wszystko ogarniałem za pomocą serwisów. W DDD jednak postulują aby nie używać anemicznych encji. Napotykam jednak tutaj taki problem, że nie wiem jak rozwiązać problemy typu problem z dzisiaj. Potrzebuję w kilku encjach obliczyć hash z kilku pól skalarnych. Normalnie wydelegowałbym to do usługi, ale jak rozumiem tym powinna się zając encja bo w sumie to jest logika domeny w moim przypadku.
Nie za bardzo chyba wypada wstrzykiwać coś do encji przez konstruktor. Użycie metod statycznych z innej klasy też nie jest eleganckie. Zatem jak rozwiązać ten problem? Traits? A co jeśli logika byłaby trochę bardziej zaawansowana i np. musiałbym wykorzystać jakiś serwis w kalkulacjach? Tak jak pisałem wg mnie normalnie powinno się wstrzyknąć jakiś serwis i użyć jego, ale w encjach raczej konstruktor powinien być używany do tworzenia samej encji.

W wielu przykładach widziałem np. używanie validatorów za pomocą metod statycznych - kurcze nie jestem jakoś fanem używania metod statycznych. i używanie w ten sposób walidatora mnie boli

Na przykładowych aplikacjach wszystko wygląda bardzo fajnie, bo nie ma tam zbyt wiele logiki biznesowej, ale w prawdziwych encjach może tego się trochę nazbierać.

Druga sprawa to długość takich klas entity - nie trudno mi sobie wyobrazić encję, która będzie miała trochę cięższą logikę biznesową i jej kod urośnie do 500 czy 1000 linijek. Czy w praktyce stosuje się zatem jakieś rozwiązania pozwalające wydzielać tą logikę do subklas?

1

A co jest złego w metodach statycznych?
Myślałeś o fasadzie jak w Laravel?
Piszesz sobie klasę, która będzie odpowiedzialna za hashowanie z normalnymi, nie statycznymi metodami, a później podpinasz ją do fasady i wtedy miałbyś:

MojaFasada::hash(...$args);

Przecież metody statyczne, też są stworzene w jakimś celu, nie ma zakazu ich używania.

4

@hadwao można np. zrobić coś w stylu:

class MyEntity{
    public Result myMethod(Function<X,Hash> hashCalculator){
        return hashCalculator.apply(X.builder().withField(this.x).withField(this.y).build());
    }
}

Chyba ze ten hashCalculator jest zawsze taki sam, wtedy można go wrzucić do tej encji jako jeden z parametrów na stałe, niemniej nie przekazywałbym jakiegoś serwisu a raczej jakiś bardzo wąski interfejs.

nie trudno mi sobie wyobrazić encję, która będzie miała trochę cięższą logikę biznesową i jej kod urośnie do 500 czy 1000 linijek

Czemu nie zamknąć tej logiki w jakąś pod-klasę i użyć kompozycji w takiej sytuacji?

0

@omenomn2: nie jestem zbytnim fanem metod statycznych bo ukrywają zależności i ogólnie jakoś mi się kojarzą z kiepskim OOP. Choć faktycznie w tym przypadku, gdyby mówić tylko o czystych funkcjach bez modyfikacji stanu etc to jeszcze nie jest taki problem.

@Shalom - przekazane kalkulatora w parametrze funkcji ma w sumie sens, choć trochę komplikuje kod kliencki korzystający z tej encji.
Co do wspomnianej przez Ciebie kompozycji to tu właśnie jest problem, że musiałbym w teorii wstrzyknąć do encji coś przez konstruktor, czego jak rozumiem w encji raczej nie powinienem robić bo konstruktor służy do inicjalizacji wartości poszczególnych pól.

2
  1. Jest kilka rozwiązań i każdy będzie miał swoją preferencję. W zależności od szczegółów można by
    a) użyć serwisu domenowego, który będzie za to odpowiedzialny. Wydaje mi się że nie w tym przypadku.
    b) może jakaś klasa bazowa i w niej trzymanie tej logiki? Może to coś co każda encja powinna mieć by ułatwić pracę z kodem?
    c) trudno, jak mus to mus i trzymasz tę logikę w każdej encji... ale to dość słabe
    d) to co @Shalom zasugerował. Czyli wstrzykujesz nie do konstruktora, ale do metody (stosując jakąś abstrakcję).
    A w ogóle do czego CI ten hash potrzebny?

  2. Jak masz tak dużą encję to albo musisz i tyle(mało prawdopodobne), albo może warto ją podzielić? I tutaj albo możemy ją podzielić
    a) na mniejsze encje, tworzące jakąś hierarchię w agregacie,
    b) na kilka agregatów - po prostu czasem nie widzimy że dany agregat tak naprawdę powinien zostać podzielony na np dwa w osobnych bounded kontekstach.
    To jest normalne bo domeny się uczymy tworząc kod i dzielenie zwłaszcza na bc jest bardzo trudne.DO tego dochodzą zmiany wymagań itd.
    Pamiętaj, że na koniec liczy się przede wszystkim by kod działał i dało się go łatwo utrzymywać.
    Jako że pytanie abstrakcyjne to i odpowiedź bez konkretów :-)

Co rozumiesz przez kod kliencki korzystający z encji?

0

@AreQrm: tak jak prosiłeś trochę szerzej opiszę co robię.

W skrócie mam system centralny, którzy przetwarza informacje o produktach pozyskując je z różnych źródeł. Następnie gdy coś się zmieniło to wrzucą tą informację na kolejki - jest to JSON z pełnym snapshotem produktu.
Do systemu centralnego jest podpiętych wiele aplikacji, które są zainteresowane zmianami w produkcie ALE każda z tych aplikacji interesuje się innym zakresem danych - np. system polski nie interesuje się zmianami w wersji niemieckiej opisu etc.

Dlatego każdy z tych systemów klienckich kalkuluje sobie hash informacji, które go interesują i zapisuje ten hash w bazie. Następnym razem gdy odbierana jest kolejna wiadomość w systemie tworzona jest nowa Encja i sprawdzany jest hash poprzedni z tym obecnym. Jeśli w kontekście danego klienta encja się nie zmieniła to jest ona ignorowana.

Zastanawiałem się czy ten hash to bardziej serwis czy logika biznesowa encji i do tej pory w sumie nie potrafię jednoznacznie na to odpowiedzieć bo z DDD dopiero zaczynam. W sumie to nawet jeszcze nie zaczynam a jedynie piszę sobie testową encje aby zobaczyć jakie problemy napotkam w prawdziwym świecie bo w tutorialach to każdemu ładnie wychodzi ;-)

Oczywiście nie chciałbym tutaj wątku ograniczać do tej jednej sytuacji, bo chętnie poznam całą paletę rozwiązań jakie się stosuje, ale z drugiej strony też chętnie się dowiem jakbyś podszedł do tej konkretnej sytuacji.

1

TL;DR
Jeśli to nie DDD to nie traktuj tak tego.
Jeśli to DDD to albo trzymaj tę logikę w metodzie typu Update(...) i zwróć czy trzeba czy nie, albo wstrzyknij do metody. Najlepiej jakby się okazało że dane do zmiany to ValueObjecty i można je ze sobą porównać i to one mają trzymać te logikę.

Pełna odpowiedź:
Nie wiem ile wspólnego z samym DDD ma ten problem, ale ja widzę to tak, proszę popraw mnie jeśli źle zrozumiałem. Pokręcę się trochę na około i dojdę do sedna, w którym teraz jesteś pewnie, ale popraw mnie jeśli gdzieś się mylę w założeniu po drodze. MOże to też innym pomoże rozkminiać takie problemy.
(Pomijam główny system na razie) Jest n systemów(albo mikro/makro serwisów jeśli chcemy myśleć o tym w ten sposób), każdy osobno. Jeden z nich (albo każdy, teraz to nie jest ważne) system odczytuje sobie dane z kolejki - te dane to nie jest encja, ale jakieś DTO. Masz też w tym systemie ... ok nazwijmy to Encje. Twój problem to czy należy tą encję zaktualizować, czy nie. I chcesz to zrobić, przez stworzenie nowej encji w kodzie i wywołać porównanie (nie wiem w jakim języku piszesz) np existin == newone albo existing.Equals(newone).
W takim wypadku, jeśli chcemy operować na encjach i koncepcjach z DDD, jedną z podstawowych cech encji jest to, że można je porównać ze sobą na podstawie samego ID, a nie wartości jakie w tym momencie posiada. Posiadanie w systemie dwóch wersji tego samego agregatu z różnymi wartościami to przepis na katastrofę. Generalnie koncepty takie jak Agregate root i Bounded context są po to, żeby tego uniknąć. To czy należy zaktualizować obiekt w bazie danych jest problemem z dziedziny technicznej, a nie domeny biznesowej. Owszem, logika kiedy należy zaktualizować - to jest biznesowa reguła, ale to co chcesz wykonać brzmi jak stricte techniczne. Niemalże CRUD. Ale to może być tylko ułamek systemu i reszta jest skomplikowana, więc psotaram się nie mieć żądnych założeń i odpowiem w dwóch wersjach:

Więc tutaj zostaje mi zasugerowanie albo czegoś stricte technicznego - nie traktuj tego jako encja w DDD, tylko użyj klasy, która może w zasadzie poosiadać tę logikę porównywania do siebie dwóch instancji. I w zależności od rezultatu zapisz lub nie. Dlaczego nie? Ano tutaj pojawia się argument - bo jeśli ten system rzeczywiście używa DDD i to jest encja w agregacie, to powinieneś mieć dostęp tylko przez agregat. I to jest też coś co pominąłeś w swojej wypowiedzi. Rozumiem że chcesz zaktualizować Encję, jeśli to jednocześnie agregat - to spoko. Jeśli nie pamiętaj, że przez ten agregat powinieneś przejść, bo on wyznacza granice dostępu i manipulacji swoimi wartościami. I to założenie w dalszej części wypowiedzi przyjmę. Założę też, że masz już rozwiązany problem konkurencyjności i nie możesz nadpisać stanu agregatu z innej transakcji. (tutaj znów jest mnóstwo rozwiązań technicznych zależnych od problemu).

Mam wiadomość z kolejki, jakiś serwis w aplikacji ją przetwarza(MessageHandler? któy jest też w kontekście OnionArchitecture serwisem aplikacyjnym), ładuje mi agregat z bazy(czy tam skąd trzeba) i wywołuje sobie na niej metodę Update(nazwa powinna być lepsza, plus tu lista parametrów, albo DTO i później
a) metoda jak trzeba to zaktualizuje co tam chce. Żadnego sprawdzania, nadpisanie wartości. Zapisz i zapomnij. Nie ważne czy zmiany były czy nie. To podejście jest naiwne i sprawdzi się tylko jak tych aktualizacji jest mało. Inaczej niepotrzebnie będzie obciążać system.
b) Jako że chcesz wiedzieć czy zapis jest potrzebny czy nie, tutaj pojawia się problem logiki aplikacji, a nie logiki biznesowej. Teraz chciałbyś wiedzieć na poziomie aplikacji czy chcesz robić update i zapis do bazy, czy nie. I tutaj jesteś, i założyłeś ten wątek.

Metoda z porównaniem i nadpisaniem Equals odpada w przypadku encji. Ale co z value objectami? Value object z definicji nie ma identyfikatora a porównujemy przez wartość. Może Twoja encja posiada Value objecty które możesz porównywać, one powinny posiadać nadpisane Equals, i wtedy mamy agregat który ma ma metodę (nazwa z tyłka, zamiast jednego można przesłać więcej value objectów typ zwracany też można rozbudować) np bool Update(ValueObject produkt) i ona tylko sobie porówna te dwa produkty i zamieni jeśli są inne. Jako rezultat zwróci Ci wartość czy dokonał tej aktualizacji, czy nie. Tutaj poszliśmy na skróty trochę, bo moglibyśmy to rozbić na dwie metody NeedsUpdate i Update, ale myślę że to sobie sam ogarniesz co wolisz.
Jeśli jednak to zdecydowanie nie jest Value object i nie przedstawisz tak tego w swojej domenie... To dalej podejśćie z tymi dwiema metodami (albo jedną) jest OK. I tutaj możesz wstrzyknąć właśnie ten HashCalculator, albo trzymać te logikę w metodzie. To zależy od Twoich preferencji i tego co łatwiej będzie otestować.

1

@AreQrm na wstępie wielkie dzięki za tak szczegółowe objaśnienie. Na pewno będę do tego wracał za jakiś czas.

Co do mojego problemu to chyba trochę niezdarnie opisałem. System nad którym teraz pracuje nie jest DDD i na pewno nigdy nie będzie. Cały temat DDD jest na razie tylko w mojej głowie - ostatnio tworzę trochę takich jakby mikroserwisów (nie do końca dobre słowo, bo to bardziej takie małe apki wspomagające procesy) i tak w ramach "samorozwoju" postanowiłem jeden z takich serwisów przepisać na coś w stylu DDD/hexagonu - taki projekt do szuflady po godzinach. Są to dla mnie nowe rzeczy, wiec zacząłem od książki + kombinuję jak ogarnąć pewne elementy i np. pisząc w obecnym systemie obsługę tej synchronizacji zastanawiałem się jakbym to rozegrał w tym moim klonie.

Po przeczytaniu Twojego komentarza dochodzę do wniosku, że faktycznie w przypadku tego konkretnego problemu nie powinienem tworzyć encji tylko tak na prawdę obiekt DTO z którego ewentualnie encja miałaby powstać i sprawdzić i serwis powinien sprawdzić czy dane są nowe czy stare przed stworzeniem samej encji.

Co do elementów, które pominąłem w swojej wypowiedzi to w sumie składają się na nią 2 elementy - pierwsze to system o którym mówię istnieje tylko w mojej głowie (ten prawdziwy nie jest ani DDD ani hexagonem) + nie do końca jeszcze ogarniam niuanse bo jestem w trakcie lektury. Na razie mam jakiś mały widok z lotu ptaka, a teraz czytam książkę omawiającą bardziej szczegółowo i na konkretnych przykładach. Jest wysoka szansa, że pewne koncepty w ogóle jeszcze rozumiem źle. Tym bardziej więc dzięki za tak szeroki opis.

2

Nie ma za co. Jeśli chodzi o DDD to bardzo ważne jest zrozumienie że nie wszędzie pasuje. I to też przysparza mu wielu przeciwników, bo czasem na siłę wpycha się je tam gdzie nie ma na nie miejsca.
DDD jest świetną techniką tam gdzie jest skomplikowana logika. I wtedy poprawi nam kod itp. Ale tam gdzie problem jest prosty albo techniczny tylko przysporzy dodatkowych problemów. I to nie są moje słowa, ale samego autora : - ). Np teraz pracuję w czymś co nazywają warstwą integracji i tutaj nie ma miejsca na DDD.
W tym momencie polecam się skupić też na tym kiedy używać a kiedy nie.
Słowa klucze to core domain(tu warto użyć DDD) supporting domain (tu nie bardzo) i generic Domain(to w ogóle najlepiej "kupić" i nie pisać samemu).
O właśnie znalazłem ciekawy wpis na ten temat, gość rozsądnie mówi o DDD na konferencjach, warto też poszukać:
https://vladikk.com/2018/01/26/revisiting-the-basics-of-ddd/

Swoją drogą tutaj podlinkuje wpis z listą ciekawych materiałów. #shamelessplug
oprogramowaniu.pl/materialy-o-ddd

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