Wątek przeniesiony 2022-03-18 19:04 z C# i .NET przez somekind.

DDD poprawna implementacja logiki biznesowej

0

Mam takie pytanie jak powinno się w poprawny sposób implementować encje, gdzie sprawdzamy np unikalność. Na necie różnie piszą np. żeby wstrzykiwać serwis czy to jest dobre podejście?

np, tak

public class User : Entity
{
    public UserId Id { get; private set; }
    public ContactDetails ContactDetails { get; private set; }
    public string Email { get; private set; }


    public void Register(string email, ContactDetails contactDetails, IUserService userService)
    {
        if (!userService.IsEmailExists(email))
            return;

        Email = email;
        ContactDetails = contactDetails;
    }
}

Serwis mam podzielony na 3 projekty Domain / Infrastructure / API. w Api trzymam handlery i validatory.
Czy może powinno się sprawdzić w command handlerze, albo może w validatorze jak używam fluent validatora?

Albo może powinienem w klasie User utworzyć sobie tam listę prywatną Users i wstrzykiwać ich przez konstruktor i tak sprawdzać ?

public class User : Entity
{
    private List<User> _users;
    public UserId Id { get; private set; }
    public ContactDetails ContactDetails { get; private set; }
    public string Email { get; private set; }

    public User(List<User> users)
    {
        _users = users;
    }

    public void Register(string email, ContactDetails contactDetails)
    {
        if (_users.Any(x => x.Email == email))
            return;

        Email = email;
        ContactDetails = contactDetails;
    }
}

Jakie jest poprawne podejście ?

3

Ad2: W tym przypadku zaciągałbyś za każdym razem całą tabelę users. Nie jest ładne rozwiązanie :)
Ad1: A dlaczego User ma mieć pojęcie o innych userach i dbać o unikalność? Raczej sugerowałbym, że nie jest to odpowiedzialność tego agregatu, a serwisu.

0

teraz mam tak, że raczej jak sprawdzam unikalność to wstrzykuje sobie walidatora do handlera i tam to sprawdzam, ale czuje że ta logika rozlewa mi się na handlery i walidatory.

1

Wstrzykiwanie serwisu jest jak najbardziej poprawnym podejściem, tylko jego nazwa powinna być bardziej specyficzna (np IEmailChecker), bo IUserService na bank łamie (będzie łamał) zasadę I z SOLIDa i zacznie zbierać inne metody luźno powiązane. IsEmailExists to taki łamany angielski. Jak już robimy ddd to prędzej można wybaczyć trzymanie idka w typie prostym niż maila w stringu.

Najbardziej naturalnym miejscem na robienie tego sprawdzenia byłby agregat który posiada kolekcje użytkowników ale pewnie takowego nie posiadasz w domenie, i tak byś tam wstrzykiwał pewnie ten serwis.

10

Osobiście nie widzę w ogólności za bardzo sensu we wstrzykiwaniu serwisów do encji, to zawsze wygląda jak jakiś błąd na innym etapie.

Tak jak i w tym przypadku. Logika biznesowa przede wszystkim ma być spójna i prawidłowo modelować biznes. Skoro biznes wymaga unikalnych emaili użytkowników, to nie powinieneś być w stanie w ogóle utworzyć obiektu User z już zajętym emailem. A więc coś wcześniej powinno przerwać przetwarzanie takich danych. Tym czymś może być serwis aplikacyjny/handler, albo walidator, albo chociażby nawet fabryka userów.

7

Jeden rabin powie "tak", a inny powie "nie".

Wspominasz DDD więc trzeba spytać czy zdajesz sobie sprawę z różnicy między encją a agregatem, oraz czy używasz agregatów? Trochę to dziwnie wygląda jak ta klasa dziedziczy po Entity. Ja założę że wiesz co to agregat, i że w Twoim przykładzie User to właśnie agregat.

Ogólnie to przyjęte jest że tak, do metod agregatów można wstrzykiwać zależności wymagane przez dany agregat na wykonanie jego pracy. Tyle tylko że- jak sam widzisz- takie podejście spotyka się często z krytyką, i ja się z tym zgadzam. Idealnie, agregat będzie operował tylko na danych wymaganych do wykonania logiki, a nie danych i zależności używanych do wyciągania większej ilości danych. Rozwiązanie czegoś takiego jest tak naprawdę proste, ale zanim je podam wstawię tutaj też fragment wypowiedzi @neves ponieważ ona po części dotyczy tego rozwiązania, a z tym co zaproponował neves się nie zgadzam:

Najbardziej naturalnym miejscem na robienie tego sprawdzenia byłby agregat który posiada kolekcje użytkowników ale pewnie takowego nie posiadasz w domenie, i tak byś tam wstrzykiwał pewnie ten serwis.

To by oznaczało stale rosnący, wymykający się spod kontroli agregat. Agregaty powinny być ograniczone w swoim zasięgu, możliwie małe i przewidywalne. Agregat nie powinien swoimi granicami wyciekać na szerszy system, a posiadanie listy wszystkich użytkowników takim wyciekaniem właśnie bym nazwał.

Teraz dobra wiadomość: problem walidacji maila dla nowego użytkownika jest klasycznym problemem z którym często zderzają się osoby początkujące w DDD. Rozwiązanie jest proste- serwisy domenowe (domain services). Są to serwisy istniejące właśnie po to, aby w przypadkach gdy trzeba wykonać logikę biznesową wyciekającą poza jedną instancję agregatu, można było zaangażować inne zależności. Pomyśl o tym w ten sposób- instancja agregatu User to dokładnie to- jeden użytkownik, fizyczna osoba. Taki użytkownik nie wie, i nie może wiedzieć, o wszystkich innych użytkownikach w systemie. Bo dla czego i skąd miałby wiedzieć? Walidacja unikalności maila nie leży więc w gestii pojedynczego użytkownika- nie leży w gestii agregatu User. Używając innego przykładu- koszyk zamówień powinien przyjmować produkty dodawane do niego, wykonywać jakąś tam walidację, ale z pewnością koszyk nie powinien posiadać całej listy tysięcy czy dziesiątek tysięcy produktów znajdujących się w magazynie, tylko po to aby sprawdzić czy produkt może zostać dodany. Koszyk nie powinien mieć zielonego pojęcia o szerszym systemie- magazynie, produktach itp.

W tym konkretnym przypadku polecenie rejestracji użytkownika powinno więc najpierw trafić do serwis domenowego. Ten serwis sprawdzi czy adres e-mail jest unikalny (używając do tego wstrzykniętej zależności) i jeśli tak, to załaduje agregat, dostarczy do niego potrzebnych danych i zakończy pracę.

Ps: Temat powinien się znaleźć w dziale Inżynieria Oprogramowania bo zagadnienie jest niezależne od języka programowania.

0

Tylko jeśli warunek konieczny do stworzenia użytkownika będzie sprawdzany w serwisie domenowy to oznacza że będzie można stworzyć użytkownika poza serwisem domenowym w końcu będzie miał dostępny konstruktor w obrębie modułu który nie sprawdza tego warunku. Zgadza się użytkownik nie wie i nie może wiedzieć o innych użytkownikach, ale nic nie stoi na przeszkodzie by mógł zapytać jakiś serwis czy email jest poprawny. Jak chcemy mieć zawsze poprawny model, to nie możemy tworzyć danej encji w zewnętrznym serwisie.

Nie wiem skąd ten wasz opór przed wstrzykiwaniem serwisów do encji, bo żadne argumenty nie padają, poza subiektywnymi odczuciami.

Dany agregat jak najbardziej może zawierać referencje to innych agreagat rotów, i bynajmniej nie oznacza że jakoś bardzo rośnie. W idealnym modelu byśmy mieli tylko jedno repozytorium, do pobrania nadrzędnego agreagat roota i całe operowanie na modelu musiało by się przez niego odbywać. Ale że jesteśmy leniwi, i jest to trudno osiągalne, zwykle kończymy z jednym repozytorium per agregat root.

1

@neves:

Tylko jeśli warunek konieczny do stworzenia użytkownika będzie sprawdzany w serwisie domenowy to oznacza że będzie można stworzyć użytkownika poza serwisem domenowym w końcu będzie miał dostępny konstruktor w obrębie modułu który nie sprawdza tego warunku. Zgadza się użytkownik nie wie i nie może wiedzieć o innych użytkownikach, ale nic nie stoi na przeszkodzie by mógł zapytać jakiś serwis czy email jest poprawny. Jak chcemy mieć zawsze poprawny model, to nie możemy tworzyć danej encji w zewnętrznym serwisie.

Zwróć uwagę że piszesz o zupełnie innej kwestii- Ty mówisz o konstruktorze, a OP między innymi o metodzie Register. Tak czy inaczej, moim zdaniem to takie doszukiwanie się problemu na siłę. Jak już pisałem- użytkownik jako instancja/obiekt może zostać stworzony bez sprawdzania tego warunku (unikalność adresu e-mail), ponieważ warunek ten nie jest niezmiennikiem konkretnego użytkownika. Jest to warunek dotyczący użytkowników, a więc coś co obejmuje wiele instancji danego agregatu (patrz obrazek poniżej). Dla tego miejsce takiej zasady jest w serwisie domenowym. A widzenie w tym problemu dla tego że- z technicznego punktu widzenia- można utworzyć instancję użytkownika z niepoprawnym adresem e-mail jest doszukiwaniem się problemu na siłę, i niepotrzebnym wciskaniem w agregat zasady która tego agregatu bezpośrednio nie dotyczy. Trzeba pamiętać że mówiąc o agregacie należy myśleć o konkretnej, jednej instancji tego agregatu w czasie.

Nie wiem skąd ten wasz opór przed wstrzykiwaniem serwisów do encji, bo żadne argumenty nie padają, poza subiektywnymi odczuciami.

Ależ ja napisałem że teoretycznie takie zależności można wstrzykiwać- chociaż zgodnie z purystyczną teorią należy podkreślić że mowa o dostarczaniu takich zależności do metod agregatu, a nie do jego konstruktora. To prawda, część oporów to zapewne subiektywne odczucia, ale przecież ja podałem konkretny powód. A w zasadzie dwa:

  • Unikając dostarczania takich zależności mamy "czysty" agregat który operuje na konkretnych danych. Wszelkie dane które są do niego dostarczone wykonuje się z założeniem że zostały wcześniej zweryfikowane (np. unikalność e-mail w serwisie domenowym) jeśli była taka potrzeba, więc agregat wykonuje tylko swoją logikę biznesową, i egzekwuje swoje zasady.
  • Jak już pisałem, w tym konkretnym przypadku sprawdzenie unikalności adresu e-mail zwyczajnie nie leży w gestii agregatu User.

Dany agregat jak najbardziej może zawierać referencje to innych agreagat rotów, i bynajmniej nie oznacza że jakoś bardzo rośnie.

To że może zawierać, nie znaczy że zawsze jest to właściwe podejście. Pisząc bynajmniej nie oznacza że jakoś bardzo rośnie wydajesz się całkowicie ignorować kontekst w jakim rozmawiamy. W tym kontekście mowa o użytkownikach w systemie, a ich potencjalna ilość jest teoretycznie nieograniczona. Nie za bardzo również rozumiem co referencje do innych agregatów mają z wspólnego z zasadą unikalności adresów e-mail. Mam również nadzieję że przez "referencje" masz na myśli IDs, a nie referencje do obiektów. Bo to drugie podejście jest zdecydowanie odradzane, chyba że są jakieś edge case'y które są dobrze argumentowane. Ja sam w realnych systemach nigdy nie zderzyłem się z potrzebą takich referencji, bo zawsze było mniej ryzykowne (transakcyjność w obrębie jednego agregatu) rozwiązanie.

W idealnym modelu byśmy mieli tylko jedno repozytorium, do pobrania nadrzędnego agreagat roota i całe operowanie na modelu musiało by się przez niego odbywać. Ale że jesteśmy leniwi, i jest to trudno osiągalne, zwykle kończymy z jednym repozytorium per agregat root.

Tutaj w ogóle nie wiem co masz na myśli.

screenshot-20220318122941.png

0

dzięki za wskazówki :)

1

Masz niezmiennik który dotyczy zmiany stanu danej encji, nie możesz przesunąć go gdziekolwiek indziej poza tą encje, bo to oznaczałoby że można model wprowadzić w niespójny stan. Możemy tak zrobić, ale nie możemy już wtedy mówić że nasz model jest zawsze poprawny, a to jest całkiem spora zaleta ddd. Nie ma znaczenia czy to jest konstruktor czy metoda, jest to zmiana stanu wewnętrznego chronionego przez niezmiennik.

Pierwszy raz słyszę o czystych agregatach, i o tym żeby ddd było rasistowskie :D. W niebieskiej książce nie ma o tym ani słowa. Zgadza się sprawdzenie nie leży w gestii tej encji, i ta encja nie dokuje tego sprawdzenia, serwis dokonuje sprawdzenia, encja tylko odpytuje serwis.

Tak, to już było poza kontekstem zupełnie, i w tym wypadku ciężko by było znaleźć encję która byłaby odpowiedzialna za przechowanie wszystkich użytkowników i nimi zarządzanie.
Jeśli byśmy chcieli być do bólu purystycznie obiektowi, to tak powinniśmy używać referencji, ponieważ idki nie istnieją w modelu biznesowym. Vladimir Khorikov jest chociażby zwolennikiem tego podejścia. Osobiście preferuje idki z lenistwa i wygody.

1

Pierwszy raz słyszę o czystych agregatach, i o tym żeby ddd było rasistowskie :D.

Tak na wstępnie- nie musisz się robić emocjonalny (vide próba lekkiego sprowadzenia czegoś do żartu). Zwyczajnie się wymieniamy opiniami, naprawdę nie trzeba przybierać defensywnej postawy. Jeśli nie zrozumiałeś co miałem na myśli przez "czystość" w tym konkretnym kontekście to trudno.

W niebieskiej książce nie ma o tym ani słowa.

Tak, i Evans sam się nawet wypowiedział że jednej z rzeczy której według niego najbardziej brakuje w jego książce to właśnie to. Źródła nie mam bo czytałem to już jakiś czas temu. Vernon to uzupełnił w swojej książce Implementing DDD.

Zgadza się sprawdzenie nie leży w gestii tej encji, i ta encja nie dokuje tego sprawdzenia, serwis dokonuje sprawdzenia, encja tylko odpytuje serwis.

Za bardzo się teraz rozdrabniamy- wiedza o konieczności sprawdzenia czegoś to również element tego sprawdzania.

Masz niezmiennik który dotyczy zmiany stanu danej encji, nie możesz przesunąć go gdziekolwiek indziej poza tą encje, bo to oznaczałoby że można model wprowadzić w niespójny stan. Możemy tak zrobić, ale nie możemy już wtedy mówić że nasz model jest zawsze poprawny, a to jest całkiem spora zaleta ddd.

Myślę że już to dokładnie wytłumaczyłem więc nie będę się znowu rozpisywał na ten temat. Stan agregatu użytkownika nie będzie niespójny. Owszem, stan szerszego systemu byłby taki, ale od tego są właśnie serwisy domenowe żeby temu zapobiegać.

Co do Khorikov'a to bardzo ciekawe że go wspomniałeś. Nie wiedziałem że on jest tego zwolennikiem, natomiast jeśli mowa o tym artykule to faktycznie- wychodzi na to że jest on zwolennikiem referencji do obiektów. Od razu dostrzegam tam jednak pewien błąd myślowy bo z jednej strony mowa o relacjach między agregatami, a z drugiej o referencjach do encji. Co na pierwszy rzut oka może wydawać się nieznaczące, ale może prowadzić właśnie do niepotrzebnego i ryzykownego mieszania się i przenikania granic transakcyjności. Ale to oddzielny temat, można by długo dyskutować. Ja po prostu się nie zgadzam z takim podejściem. Zresztą nie tylko ja: rozdział Rule: Reference Other Aggregates By Identity. Vernon'a ogólnie polecam bo nie tylko objaśnia jakieś zagadnienia, ale ma również pragmatyczne podejście i wprost przedstawia różne scenariusze kiedy łamanie konkretnych zasad może mieć sens.

Notabene, przywoływany przez Ciebie Khorikov również jest zwolennikiem używania serwisów domenowych do takich zasad jak unikalność maila użytkownika. To właśnie z jego artykułu wziąłem powyższy diagram:

  • Aggregates are responsible for maintaining consistency boundaries.
  • There are two types of invariants: those that are confined to separate aggregate instances and those that span across all of them.
  • The first type should be attributed to aggregates.
  • Invariants of the second type should be handled by domain or application services.

Tak czy inaczej, nie zgadzamy się w pewnych fundamentalnych kwestiach dotyczących granic agregatów i ich transakcyjności. Chyba tak to można podsumować, i nie ma co dalej ciągnąć ten temat.

8

Dodam tylko od siebie, że abstrahując od DDD logika

if(!emailExists(user.email)) {
  save(user);
}

jest podatna na wyścig. Ale to pewnie oczywiste, informuję na wszelki wypadek :)

0
Aventus napisał(a):

Tak na wstępnie- nie musisz się robić emocjonalny (vide próba lekkiego sprowadzenia czegoś do żartu). Zwyczajnie się wymieniamy opiniami, naprawdę nie trzeba przybierać defensywnej postawy. Jeśli nie zrozumiałeś co miałem na myśli przez "czystość" w tym konkretnym kontekście to trudno.

To był zwykły żart dotyczący niefortunnego tłumaczenia słowa "purity" na język polski, a także tego że to nie jest powszechnie używany termin w połączeniu z ddd, nie doszukuj się czego czego tam nie ma.

Aventus napisał(a):

Myślę że już to dokładnie wytłumaczyłem więc nie będę się znowu rozpisywał na ten temat. Stan agregatu użytkownika nie będzie niespójny. Owszem, stan szerszego systemu byłby taki, ale od tego są właśnie serwisy domenowe żeby temu zapobiegać.

Serwis domenowy nie jest w stanie ochronić poprawności stanu systemu skoro możemy wprowadzić encję w błędny stan przez pominiecie korzystania z serwisu.

Aventus napisał(a):

Notabene, przywoływany przez Ciebie Khorikov również jest zwolennikiem używania serwisów domenowych do takich zasad jak unikalność maila użytkownika. To właśnie z jego artykułu wziąłem powyższy diagram:

O patrz a w nowszym artykule jakoś Vladimir nie ma problemu z robieniem tego sprawdzenia w encji:
https://enterprisecraftsmanship.com/posts/domain-model-purity-completeness/
(pomijając już kompletnie że jego przykładowy kod z wstrzykiwaniem całych repozytoriów jest bardzo ewidentnym łamaniem solida)

Zresztą ten artykuł doskonale obrazuje że model obiektowy może posiadać różne cechy : purity , completeness , performance i ja dorzucam do tego correctnes(always valid). I pokazuje to że nie możemy mieć ich wszystkich i musimy dokonać jakiegoś trade-offu. I cała nasza dyskusja jest o tym czy lepiej jest mieć pure domain model czy always valid domain model. Ja preferuje drugą opcję w szczególności przy skomplikowanych modelach, do których kod operujący na tych modelach mogą pisać analitycy domenowi.

0

@neves

Serwis domenowy nie jest w stanie ochronić poprawności stanu systemu skoro możemy wprowadzić encję w błędny stan przez pominiecie korzystania z serwisu.

Znów się powtórzę- instancja agregatu nie będzie się znajdować w błędnym stanie, ponieważ w gestii pojedynczego użytkownika nie jest dowiadywanie się o szerszym systemie i upewnianie że jakaś wartość jest unikalna w kontekście szerszego systemu. W tym przypadku szerszym systemem są wszyscy zarejestrowani użytkownicy. Co innego gdyby był agregat RegisteredUsers gdzie zasady unikalności w kolekcji użytkowników miałyby sens- ale taki agregat to moim zdaniem zły pomysł o czym już pisałem wcześniej. I raz jeszcze się powtórzę że jeśli tego nie dostrzegasz lub się z tym nie zgadzasz to mamy fundamentalnie inne podejście w tej kwestii więc nie ma co drążyć.

O patrz a w nowszym artykule jakoś Vladimir nie ma problemu z robieniem tego sprawdzenia w encji:
https://enterprisecraftsmanship.com/posts/domain-model-purity-completeness/
(pomijając już kompletnie że jego przykładowy kod z wstrzykiwaniem całych repozytoriów jest bardzo ewidentnym łamaniem solida)

Tyle tylko że on tam używa tego jako przykład- jak sam napisałeś zresztą. To ma być proste żeby nie rozpraszać czytelnika. Bierzesz jakiś prosty przykład i zakładasz że to czemuś udowadnia, podczas kiedy ten artykuł jest o czymś fundamentalnie innym- o czystości i kompletności między warstwami aplikacji (serwisy aplikacji) i domeny (agregaty). Agregaty i serwisy domenowe z kolei należą do tej samej warstwy, i stoją na straży zasad biznesowych. Innymi słowy- warstwa domeny jako całość zachowuje kompletność. Nie widzę żeby on tam dał disclaimer "zapomnijcie o tym co pisałem o serwisach domenowych", tak samo jak we wcześniejszym artykule nie widzę "to jest nieaktualne". Nie wiem więc skąd sugestia że podlinkowany przez Ciebie artykuł miałby w jakikolwiek sposób unieważniać kwestię serwisów domenowych. Co najwyżej powiedziałbym że to uzupełnia. A jeśli taki zamiarem było unieważnienie to wychodziłoby na to że Khorikov sam sobie przeczy, co zresztą nie byłoby czymś niezwykłym- nie jest nieomylną wyrocznią, jest tylko człowiekiem.

Zresztą ten artykuł doskonale obrazuje że model obiektowy może posiadać różne cechy : purity , completeness , performance i ja dorzucam do tego correctnes(always valid)

Szczerze mówiąc nie widzę żebyś tutaj dorzucał coś nowego, chyba po prostu inaczej nazywasz to co już opisane w tamtym artykule. Ale tak, są różne podejścia i z tym się zgadzam. Ja jestem zwolennikiem skupienia się na poprawnym modelowaniu agregatów, gdzie pierwszą i podstawową zasadą jest ich granica transakcyjności, a coś co wykracza poza granicę jednej instancji powinno zostać obsłużone przez serwis domenowy tak, aby z założenia agregat zawsze dostawał poprawne dane wejściowe (oczywiście te dane nadal mogą łamać zasady konkretnego agregatu, i muszą zostać pod tym względem zweryfikowane). Rozdrabnianie się na to że mając serwisy domenowe i agregaty ryzykujemy że agregat znajdzie się w złym stanie jest dla mnie przejawem złego modelowania agregatów, oraz niepotrzebnego doszukiwania się problemów- o czymś podobnym pisałem w podlinkowanym przez @1a2b3c4d5e wątku.

Można by debatować w nieskończoność ale to raczej przy piwie niż na forum także pozdrawiam i EOT z mojej strony :)

1

@Aventus: a takie pytanie jeszcze, czy wstrzykiwać interfejs repozytorium do serwisu domenowego to jest dobre podejście czy nie bardzo?

public class UserService
{
    private IUserRepository _userRepository;

    public UserService(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }


    public async Task<User> RegisterUser(string email)
    {
        if (await _userRepository.IsEmailAlreadyExists(email))
            throw new Exception();

        return new User();
    }
}
2

Moim zdaniem to jest jak najbardziej ok, i dokładne to o czym pisałem wcześniej. Wręcz ciężko sobie wyobrazić aby tego nie robić, bo przecież w wielu przypadkach serwis domenowy będzie musiał załadować agregat i wykonać na nim operację.

0

@Kubuś Puchatek: Mówisz o wstrzykiwaniu repozytorium do serwisu domenowego, ale w Twoim przykładzie na pierwszy rzut oka wygląda to na serwis aplikacyjny bardziej ze względu na nazwę. Jestem zwolennikiem dodawania suffixa do serwisów domenowych, aby móc je jawnie odróżnić od aplikacyjnych (wiem, że namespace jest inny, ale mimo wszystko dość łatwo na pierwszy rzut oka patrząc na kod to pomylić). W tym przypadku bardziej widziałbym nazwę tego serwisu jako UserDomainService, albo coś bardziej wymownego.

2

Jest to jeden z powodów dla których tworzymy serwisy domenowe, mianowicie żeby wykonać logikę biznesową, która wymaga zależności z zewnątrz. Dzięki temu nie trzeba nic wstrzykiwać do agregatu. Inną kwestią, ze nazwa serwis domenowy jest umowna i w moim mniemaniu klasy będące takimi serwisami nie muszą się nazywać *Service, a mieć bardziej wymowną nazwę, mającą swój odpowiednik w języku wszechobecnym.

0
Charles_Ray napisał(a):

Dodam tylko od siebie, że abstrahując od DDD logika

if(!emailExists(user.email)) {
  save(user);
}

jest podatna na wyścig. Ale to pewnie oczywiste, informuję na wszelki wypadek :)

Jak powinno się poprawnie zaimplementować walidację unikalności e-maila jeśli mamy bazę typu NoSQL?

1

@nobody01: ah, aż przypomina mi się burza jaką rozpętałem wątkiem o bazach SQL vs NoSQL :) Oczywiście implementuje się to zależnie od tego co oferuje dana baza NoSQL. Np. jakieś operacje compare exchange, albo utrzymywanie kolekcji gdzie unikalnym kluczem jest właśnie adres email i przeprowadzanie operacji insert (nie upsert).

0

a tu nie wystarczy objąć ten fragment lockiem (jeżeli 1 instancja)?

2

@nobody01:

  1. Unique index
  2. Operacje atomowe w stylu https://www.mongodb.com/docs/manual/reference/method/db.collection.findAndModify/ posiadają również bazy NoSql, tylko problem zwykle dotyczy zasięgu transakcji

@1a2b3c4d5e:

  1. Wtedy wszystkie operacje tworzenia użytkownika będą musiały czekać, nawet jak są różne loginy
  2. Nie da się skalować takiej aplikacji
1

Cały ten wątek to bardzo dobry przykład jak skomplikować sobie coś bardzo prostego czym jest DDD i czym to się kończy - jeszcze gorszym i bardziej zawiłym kodem. Są różne wymagania biznesowe, możesz mieć wymaganie biznesowe, że w przypadku błędnego requestu http masz zwracać błąd 400 - też będziesz na siłę próbował to implementować w domenie?

@Kubuś Puchatek Mam takie pytanie jak powinno się w poprawny sposób implementować encje, gdzie sprawdzamy np unikalność. Na necie różnie piszą np. żeby wstrzykiwać serwis czy to jest dobre podejście?
Serwis mam podzielony na 3 projekty Domain / Infrastructure / API. w Api trzymam handlery i validatory.
Czy może powinno się sprawdzić w command handlerze, albo może w validatorze jak używam fluent validatora?

Jedynym poprawnym miejscem, gdzie powinieneś to zaimplementować to bezpośrednio w implementacji IUserRepository: I tam w zależnosci od użytej bazy (bo zakładam ze jej używasz) albo w transakcji sprawdzasz czy user istnieje, albo dodajesz constrainty na bazie, albo jakieś operacje atomowe itd.
Twój serwis IUserRepository wystawia jedna metodę Task<User> registerUser(string email) i domena ma wywalone na sprawdzanie tej unikalności - jest to odpowiedzialność innego modułu, w tym wypadku infry/bazy danych, który będzie implementować metodę registerUser

I tak na przyszłość - jeśłi chcesz z powodzeniem zacząć używać DDD olej te filozoficzne dywagacje o jakiś agregatach i na początek skup się po prostu na:

  1. Dobrym zaprojektowaniu interfejsów między domeną a pozostałymi modułami - czyli co jest odpowiedzialnoscia danego modułu - ten punkt np. pozwolił rozwiązać twoj problem z unikalnością - czyli decydujesz, że to odpowiedzialnością repo jest definicja jak przechowywać duplikaty a nie domeny.
  2. Zacznij rozwiązywać problemy/pisać nowe funkcjonalności zaczynając od domeny. Na początek możesz tworzyć proste mocno anemiczne struktury danych i serwisy które na tych strukturach operują - potem zobacz jakie metody można przerzucić do tych struktur (na początek np takie metody, które operują tylko na danych z tego obiektu) - np dla klasy Circle będzie to policzenie pola.

I to w zasadzie na tyle, dzięki pkt 1. będziesz miał czystą heksagonalną architekture czyli już i tak lepiej niż 90% projektów. Dzięki 2. twoje modele będą przyjaźniejsze do użycia i nie będziesz potrzebował tylu statycznych klas i serwisów do każdej naprostszej operacji na danym obiekcie.

1

Na jakiej podstawie stwierdzasz, że sprawdzanie reguły X jest odpowiedzialnością infrastrutury a nie domeny?

0

Kurczak. Mam problem z tym podejściem: "wszystkie reguły domenowe powinny być sprawdzane w domenie".

Czy adres e-mail już istnieje? Co jest w tym wypadku najlepszym źródłem prawdy? Więzy integralności w bazie relacyjnej lub nierelacyjnej.

Zamiast:

if(!emailExists(user.email)) {
  save(user);
}

To:

save(user); // bah! Wyjątek, jeśli użytkownik z takim adresem e-mail już istnieje. Naruszono unikalny indeks. Można nawet ten wyjątek przepakować w jakiś biznesowy, bo kontekst wywołania jest znany :-)

Bo... Można kruszyć kopię i dyskutować o agregatach, separacji domeny od infrastruktury, a i tak często kończy tym, że w agregacie czy innej encji wycieka bazodanowa wersja (@Version), która jest wykorzystywana do mechanizmu optymistycznego blokowania. Cała separacja domeny od infrastruktury jak krew w piach 😉

0

@Bronzebeard:

Ciężko dywagować bez znajomości twojej domeny. Strzelam, że w tym przypadku (bounded contextcie) email jest ID biznesowym (nie technicznym) użytkownika aplikacji, a User jest agregatem, więc wyszukiwanie użytkownika odbywa się po jego id biznesowym a nie technicznym (klucz główny w bazie danych), które powinno być niewidoczne/transparentne dla warstwy domenowej i wyższych. Więc to co robisz w serwisie aplikacyjnym to sprawdzasz czy użytkownik o podanym email już istnieje, jeżeli nie istnieje to go tworzysz, jeżeli istnieje to rzucasz wyjątek/zwracasz error result w zależności co preferujesz. W kodzie mogłoby to wyglądać tak:

public void RegisterUserHandler()
{
  public void Handle(RegisterUser registerUserCmd)
  {
    var user = userRepository.Find(registerUserCmd.Dto.Email);
    if (user != null)
      throw new UserExistsException($"An user with email {registerUserCmd.Dto.Email} already exists.");

    var user = registerUserCmd.Dto.ToDomainModel();

    userRepository.Save(user);
  }
}

Można kruszyć kopię i dyskutować o agregatach, separacji domeny od infrastruktury, a i tak często kończy tym, że w agregacie czy innej encji wycieka bazodanowa wersja (@Version), która jest wykorzystywana do mechanizmu optymistycznego blokowania. Cała separacja domeny od infrastruktury jak krew w piach 😉

No ale czego oczekujesz? Jeżeli twoim wymaganiem jest pełna separacja modelu domenowego od persystencji to robisz dwa modele i tłumaczysz jeden na drugi. Jeżeli nie masz takiego wymagania to idziesz na kompromis. Poza tym to o czym piszesz to Java i jej adnotacje, w .NET możesz sobie zrobić FluentMapping i nie zaśmiecać domeny adnotacjami czy jak to jest w .NET atrybutami.

0

@Bronzebeard:

Jasne. Mam dwa pytania. Pierwsze: Jak w takim przypadku obsługujesz zduplikowane dane, jeśli wymaganie biznesowe to: istnieje tylko jeden użytkownik z tym samym adresem e-mail? Masz jakiś job kompensujący, który wymiata zduplikowane dane? Jak wtedy zdecydować, którego użytkownika usunąć? Usunąć tego, który został dodany później? Drugie: Jeżeli twoim wymaganiem jest pełna separacja modelu domenowego od persystencji to robisz dwa modele i tłumaczysz jeden na drugi. czyli przypisujesz version w version?

Ad. 1
Dla takich prostych przypadków jeżeli mam pod spodem bazę która umożliwia utworzenie unique constraint na kolumnie w twoim przypadku Email w tabeli Users to tak właśnie robię. W sytuacji gdy nastąpi naruszenie constrainta to w aplikacji leci wyjątek, który sobie obsługuję wyżej. Można się bawić w optimistic locking i zabezpieczenia na poziomie aplikacji, ale w tym przypadku nie widzę sensu skoro silnik bazy danych dostarcza mi taką funkcjonalność out of the box. Gdyby natomiast doszło do sytuacji w której dwóch użytkowników modyfikuje te same dane to optimistic locking ma więcej sensu, bo mogę przechwycić wyjątek i wyświetlić w aplikacji użytkownikowi informację, że rekord został zmodyfikowany przez innego użytkownika, nowe wartości to są takie i takie i czy chce je nadpisać.

Ad. 2
Nie znam Javy i jej Hibernate/JPA, ale twój model domenowy musi gdzieś trzymać informację po której ORM sobie wykmini czy stan obiektu się zmienił czy nie. Odniosłem się do sytuacji w której adnotacja @Version a więc szczegół implementacyjny ORMa wycieka do warstwy domenowej. Przykładowo w dotnetowym EF Core mogę sobie zrobić tak

// model domenowy, warstwa domeny
public class User
{
    public int Id { get; }

    public string FirstName { get; }

    public string LastName { get; }

    public byte[] Version { get; }
}

// impelementacja DB Context, gdzieś w warstwie persystencji
public class BlogContext : DbContext
{
    public BlogContext(DbContextOptions<BlogContext> options)
        : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<User>()
            .Property(u => u.Version)
            .IsRowVersion();
    }
}

Gdybym się uparł to mogę zrobić Version polem prywatnym co jeszcze bardziej poprawi sytuację. Więc jak widzisz obiekt domenowy posiada informację o swojej wersji bo musi, ale to w jaki sposób ta informacja tłumaczona jest na język ORM-a i bazy danych jest poza warstwą domenową. Moja warstwa domenowa wie, że istnieje pole/właściwość Version ale czy pod spodem optimistic locking jest zrobione na ORM-ie czy za pomocą mojej własnej implementacji tego już nie wie.

Dzięki konfiguracji ORM-a za pomocą Fluent API mogę zminimalizować ilość wyciekających informacji na temat użytego ORM-a do warstwy domenowej i co więcej nie potrzebuję dwóch osobnym modeli do tego. Niestety jak pisałem wyżej nie znam na tyle Hibernate/JPA żeby powiedzieć czy da się coś takiego zrobić w Javie.

1

@Bronzebeard:

Czyli jak z userRepository.Save(user); zostanie rzucony wyjątek infrastrukturalny o naruszeniu więzów integralności w bazie danych, to gdzie go interpretujesz i zamieniasz na biznesowy podobny do UserExistsException?

Wyjątek przechwytywany jest w warstwie infrastruktury a dokładnie persystencji, to co robię to po złapaniu wyjątku sterownika do bazy danych rzucam domenowy. W C# na szybko wyglądałoby to tak:

public void Save(User user)
{
  try
  {
  }
  catch (DbUpdateException ex)
  {
    if (ex.InnerException is SqlException sqlException)
    {
      switch (sqlException.Number)
      {
        case 2601: // Unique Key violation
          throw new UserExistsException("An user with given email already exists."); 
      }
    }
  }
}

Chociaż ja preferuję zwracać Result<T> zamiast rzucać wyjątek.

Wyjątek jest łapany w warstwie aplikacji a dokładnie w handlerze/serwisie który decyduje co w tej sytuacji zrobić.

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