DDD i sterowanie przepływem przez wyjątki

0

Znalazłem ostatnio aplikację napisaną zgodnie z DDD (o której pozytywnie się wypowiadaliście) i zobaczyłem, że metody w encjach dokonują walidacji argumentów, a informacje o błędach przekazują poprzez wyjątki, tak jak np. tu albo tu. No ale przecież sterowanie przepływem przez wyjątki jest złe, więc pewnie w warstwie aplikacji wykonywana jest osobna walidacja. Tylko że to oznacza duplikację kodu. Czy czegoś nie rozumiem?

1

Te switche tylko po to by zwrócić wyjątek wyglądają strasznie :D.

Wprawdzie obecnego kursu Piotra nie widziałem jeszcze, o tyle obejrzałem cały poprzedni, "Becoming a software developer". I o ile się to dobrze ogląda i słucha, i jest to w miarę przekrojowe pokazanie różnych technologi. O tyle finalny kod który powstał przez cały kurs, to wielka nie działająca kupa i zlepek różnorodnych pomysłów, który pokazuje jak można przeinżynierować prostego CRUDA i go nie dowieźć do końca ;). Także do kodu bym podchodził z dystansem, bo główna wiedza jest przekazywana w trakcie odcinka, a kod jest traktowany jako obywatel drugiej kategorii.

Co do walidacji czy też sprawdzania niezmienników, jeśli chcemy mieć zawsze poprawny model domenowy, to to jest to niesamowity ból, bez względu którą droga pójdziemy.
Używanie Exceptionów wprawdzie złe i wolne, wymaga jednak mniej pracy niż zwracanie Result.

0

W "Domenie" wyjątki powinny utrzymywać ograniczenia "niezmienniki" Gościu zrobił sobie jakąś walidacje punktu wejścia pod klienta na swichu. Poza tym większość Asercji dla constrain można się pozbyć. Pisałem już, że te przykłady to wprowadzenie do infrastruktury związanej z deploymentem - mikroserwisami, a nie wyznacznik jakości kodu. Poza tym, kto powiedział, że ten projekt ma coś wspólnego z DDD ?

0

To jakby wyglądała poprawna wersja encji Order? Tzn. może nie tyle poprawna w sensie DDD, co po prostu pragmatyczna.

2

Tu masz porównanie różnych podejść:
https://enterprisecraftsmanship.com/2016/09/13/validation-and-ddd/

w zależności od kontekstu któreś z nich może być lepsze od innego, ale prawda jest taka że każde z nich to wybór mniejszego zła, bo walidacja/niezmienniki to ciężki temat który nie doczekał się dobrego rozwiązania, temat który jest często pomijany w opracowaniach.

1
    9         public void Complete()
    8         {
    7             AssertThatCanComplete();
    6 
    5             Status = OrderStatus.Completed;
    4         }
    3 
    2         private void AssertThatCanComplete()
    1         {
 69              if (Status != OrderStatus.Available)
    1             {
    2                 throw new InvalidOperationException("Can not complete order.");                                                        
    3             }                                                                                                                          
    4         } 

plus Walidacja po stronie aplikacji

0

@neves:

Dlaczego to niby nie są dobre rozwiązania?

Dodatkowo można byłoby wykorzystać ValueTuple aby usprawnić te mechanizmy.

0

Czyli wychodzi na to, że najlepiej tak:

  • prostą walidację (np. null-check, length-check) robimy przy użyciu fluent validation
  • walidację "biznesową" robimy za pomocą dodatkowej metody walidacyjnej w encji, którą wywołujemy w warstwie aplikacji przed wykonaniem operacji (dla bezpieczeństwa wrzucamy ją też do właściwej metody wykonującej operację)

Ten transaction script dużo prostszy. :P

7
nobody01 napisał(a):

No ale przecież sterowanie przepływem przez wyjątki jest złe, więc pewnie w warstwie aplikacji wykonywana jest osobna walidacja. Tylko że to oznacza duplikację kodu. Czy czegoś nie rozumiem?

Ale tu przecież nie ma żadnego sterowania przepływem, a co dopiero przez wyjątki. Konstruktor klasy robi to, co do niego należy - czyli tworzy spójny i użyteczny obiekt, albo nie tworzy go wcale rzucając przy okazji wyjątek.
Czy to duplikacja walidacji z warstwy aplikacji? Nie powiedziałbym, to raczej dodatkowe zabezpieczenie. Co jeśli ktoś nie napisze walidatora? Co jeśli walidator nie zostanie uruchomiony? Co jeśli pojawi się nowe źródło encji niezwiązane z dotychczasową warstwą aplikacji? Potem powstaną jakieś niedorobione encje, i prędzej czy później w bazie pojawią się zepsute dane, albo skądś poleci NRE. I kilka minut oszczędności w pisaniu porządnego kodu zamieni się w wiele godzin debugowania spaghetti.

0

@somekind:

Z jednej strony tak, każda funkcja powinna mieć własną walidację, a szczególnie jeżeli pracujesz w NASA :) ale z drugiej zaczyna to dziwnie wyglądać jeżeli 80% kodu to sprawdzanie danych w niedużych appkach.

Jeżeli ktoś dobrze tego nie ogarnie pod względem wydzielenia, to będzie to koszmarne w użytkowaniu :P

0

Nieprawda, że każda funkcja - tylko te publiczne w publicznych klasach. I konstruktory obiektów. Nie zauważyłem, żeby stanowiło to 80% kodu, nie sądzę, aby stanowiło nawet 10%. Za to można oszczędzić w ten sposób na dziwnych błędach i wyjątkach na produkcji.

0
somekind napisał(a):

Nieprawda, że każda funkcja - tylko te publiczne w publicznych klasach. I konstruktory obiektów. Nie zauważyłem, żeby stanowiło to 80% kodu, nie sądzę, aby stanowiło nawet 10%. Za to można oszczędzić w ten sposób na dziwnych błędach i wyjątkach na produkcji.

Nawet przy takim prostym podziale aplikacji:

Kontroler - walidacja np. formularza np. przy użyciu fluentval., Serwis - kolejna niezależna od poprzedniej walidacja, jakieś wydzielone metody do wykonywania logiki - też mające swoją niezależną walidację

Widać, że powiela się to wiele razy, ale jest to konieczne.

No chyba, że jakiś lepszy pomysł poza rop

https://vimeo.com/97344498

https://vimeo.com/113707214

https://fsharpforfunandprofit.com/rop/

0

Jeśli chodzi o sterowanie wyjątkami to:
Po "error messages" i ilości wyjątków w swichu można dojść do wniosku, że handler na exception'y w ASP w pełni role "walidacji", sam handler na komendy podtrzymuje mnie w tym przekonaniu. https://github.com/devmentors/DNC-DShop.Services.Orders/blob/master/src/DShop.Services.Orders/Handlers/Orders/CompleteOrderHandler.cs

Walidacja formularza to nie jest to samo co wymuszenie niezmienników. Walidacja w domenie to inny rodzaj walidacji może być np. odpowiedzialny za sprawdzenie, czy aby na pewno wsadzamy produkty chemiczne, które mają właściwości wybuchowe do opancerzonego kontenera.

ROP to dobry temat na prezentacje bo wszyscy w tedy robią WOW, ale w językach czystko funkcyjnych to raczej nie nowość. Teoretycznie, jeśli zwracasz bool'a na którym robisz ifa to też masz taki mały ROP tylko, że mniej deklaratywnie :P

0
somekind napisał(a):

Ale tu przecież nie ma żadnego sterowania przepływem, a co dopiero przez wyjątki. Konstruktor klasy robi to, co do niego należy - czyli tworzy spójny i użyteczny obiekt, albo nie tworzy go wcale rzucając przy okazji wyjątek.
Czy to duplikacja walidacji z warstwy aplikacji? Nie powiedziałbym, to raczej dodatkowe zabezpieczenie. Co jeśli ktoś nie napisze walidatora? Co jeśli walidator nie zostanie uruchomiony? Co jeśli pojawi się nowe źródło encji niezwiązane z dotychczasową warstwą aplikacji? Potem powstaną jakieś niedorobione encje, i prędzej czy później w bazie pojawią się zepsute dane, albo skądś poleci NRE. I kilka minut oszczędności w pisaniu porządnego kodu zamieni się w wiele godzin debugowania spaghetti.

Tak sobie myślę, że utworzenie spójnego i użytecznego obiektu nie zawsze jest takie proste. No bo co jeśli do przeprowadzenia walidacji potrzebna jest komunikacja ze światem zewnętrznym, np. bazą? Odpowiadającego za to walidatora też możemy zapomnieć napisać albo uruchomić. Z drugiej strony, nie będziemy przecież wstrzykiwać repozytorium do encji. :P Czy oznacza to, że walidacja w encji, którą tak promuje DDD, tak naprawdę nie rozwiązuje problemu, który ma rozwiązywać?

3
nobody01 napisał(a):

Tak sobie myślę, że utworzenie spójnego i użytecznego obiektu nie zawsze jest takie proste. No bo co jeśli do przeprowadzenia walidacji potrzebna jest komunikacja ze światem zewnętrznym, np. bazą? Odpowiadającego za to walidatora też możemy zapomnieć napisać albo uruchomić. Z drugiej strony, nie będziemy przecież wstrzykiwać repozytorium do encji. :P Czy oznacza to, że walidacja w encji, którą tak promuje DDD, tak naprawdę nie rozwiązuje problemu, który ma rozwiązywać?

Repozytorium wstrzykiwać nie będziemy, ale możemy wstrzyknąć serwis domenowy który nam zwaliduje to co trzeba, zobacz akapit "BC scope validation implementation":
Domain Model Validation

0

Wstrzykiwać serwis w encje. ? O_o

BlueBook Bardzo dobrze tłumaczy w jaki sposób używać specyfikacji, wystarczy przeczytać.

Domain Model Validation

public class BusinessRuleValidationException : Exception

var allCustomers = await _customerRepository.GetAll();
var customer = new Customer(request.Email, request.Name, allCustomers);

Po prostu to przemilczę.

0
nobody01 napisał(a):

Tak sobie myślę, że utworzenie spójnego i użytecznego obiektu nie zawsze jest takie proste. No bo co jeśli do przeprowadzenia walidacji potrzebna jest komunikacja ze światem zewnętrznym, np. bazą?

Spójny i użyteczny dla mnie oznacza taki, który ma wszystkie potrzebne mu pola i zależności wypełnione sensownymi danymi. Jakoś nie mogę sobie wyobrazić sytuacji, w której do walidacji podczas tworzenia encji trzeba byłoby się komunikować z bazą, mógłbyś podać przykład?

0

A co sądzicie o takim podejściu? https://lostechies.com/jimmybogard/2016/04/29/validation-inside-or-outside-entities/ Walidacja przeniesiona jest z domeny do warstwy aplikacji, a encje przyjmują jako argument komendy. W komentarzach ktoś pokazuje, jak przy użyciu MediatR wymusić napisanie walidatora komendy i automatycznie go odpalić przed rozpoczęciem właściwego przetwarzania. W ten sposób można uniknąć duplikacji kodu związanego z walidacją.

0

To poco ci abstrakcja domeny skoro aby zrozumieć jej działanie, musisz zajrzeć do innej warstwy? Poza tym kontrakt to nie jest to samo co walidacja, której głównym celem jest komunikacja z UI. Przestań czytać te bzdury w internetach.

0

Przecież Jimmy Bogard to chyba autorytet w .NET :P Czyli co: zgodnie z biblią DDD w encji nie powinno być w ogóle kodu w stylu if(string.IsNullOrEmpty(parameter20)) throw new DomainException("Parameter 20 is not valid."), tylko sama logika biznesowa?

0

Przecież Jimmy Bogard to chyba autorytet w .NET HEHE ;D

Nie zrozumiałeś mnie, a poza tym jestem ateistą. Jest taki niedoceniony język nazywa się SpecSharp. Przejrzyj jego dokumentacje i potem powiedz mi, czym się różni walidacja od kontraktu czy asercji. Może tak zaskoczysz.

0

Talk is cheap. Show me the code. :P

0

A co gdyby zamiast rzucać wyjątki w warstwie domeny zwracać jakieś obiekty rezultatu?

        public Result Rate(string customerId, int stars)
        {
            if (_rates.Any(r => r.CustomerId == customerId))
                return new Result(ErrorType.NotValid, "Produkt został już przez Ciebie oceniony.");

            _rates.Add(Rate.Create(Id, customerId, stars));

            return Result.SuccessfulResult;
        }

Potem w warstwie aplikacji:

        public async Task<Result> Rate(RateProductDto dto)
        {
            var product = await _uow.Products.GetByIdWithRates(dto.ProductId);

            if (product == null)
                return new Result(ErrorType.NotValid, "Produkt nie istnieje lub został usunięty.");

            var ratingResult = product.Rate(_auth.GetCurrentCustomerId(), dto.Stars);

            if (ratingResult.IsSuccessful)
                await _uow.Save();

            return ratingResult;
        }

Wydaje mi się, że to proste i ładne rozwiązanie. Warstwa aplikacji jest wtedy naprawdę cienka. Do tworzenia obiektów zamiast konstruktora można by użyć factory method (chociaż to raczej nie jest dobra nazwa).

EDIT Zapomnialem dopisac walidacji danych wejsciowych.

3

no generalnie po co chciałbyś rzucać jakikolwiek wyjątek w sytuacji niewyjątkowej?

0

@WeiXiao: Spojrz na na koncowke pierwszego posta @neves

0

@neves:

Co 4 miesiące temu miałeś na myśli pisząc

Używanie Exceptionów wprawdzie złe i wolne, wymaga jednak mniej pracy niż zwracanie Result.?

Bo mi się wydaje, że jest to po prostu 1 check (if) per "warstwa" w danym procesie

0

Tego if checka można się często pozbyć, jeśli zrobi się coś w stylu:

await _uow.SaveIf(ratingResult.IsSuccessful);
0

I jak będzie wyglądał przepływ?

var result = await trololo.Something()

_blabla.DoIf(result)
_blabla.SaveIf(result)
0

No w DDD zazwyczaj jeden serwis wykonuje operacje na jednym AR, więc po prostu zazwyczaj wywołasz jakąś metodę tego AR, jeśli się powiedzie, to zapiszesz zmiany. Eventy dotyczące innych AR możesz opublikować w SaveChanges ORMa, tak jak to jest pokazane tu: https://4programmers.net/Forum/1608310

1

To może być znacznie więcej niż jeden if, jeśli w tym naszym serwisie który orkiestruje operacje na domenie, każda operacja zwraca Result, to tych ifów będzie znacznie więcej. Już nie wspominając o zwracaniu Result przez jakieś bardziej zagnieżdżone serwisy domenowe, gdzie tego Result trzeba przepychać w górę stosu wywołań.
Jakimś rozwiązaniem jest Railway Oriented Programming, no ale to już sporo tej naszej niezbyt potrzebnej infrastruktury (złożoności przypadkowej) która nie ma nic wspólnego z problemami domenowymi.

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