Domain Validation in a CQRS architecture

Niebezpieczeństwo ... Niebezpieczeństwo Dr. Smith ... Przed nami stanowisko filozoficzne

Celem tego posta jest ustalenie, czy umieszczenie logiki sprawdzania poprawności poza moimi jednostkami domeny (w rzeczywistości łączny root) daje mi większą elastyczność lub jestkod kamikaze

Zasadniczo chcę wiedzieć, czy istnieje lepszy sposób sprawdzania poprawności moich jednostek domeny. Tak mam zamiar to zrobić, ale chciałbym pańskiej opinii

Pierwsze podejście, które rozważałem, było:

class Customer : EntityBase<Customer>
{
   public void ChangeEmail(string email)
   {
      if(string.IsNullOrWhitespace(email))   throw new DomainException(“...”);
      if(!email.IsEmail())  throw new DomainException();
      if(email.Contains(“@mailinator.com”))  throw new DomainException();
   }
}

W rzeczywistości nie podoba mi się to sprawdzanie poprawności, ponieważ nawet gdy hermetyzuję logikę sprawdzania poprawności w poprawnym elemencie, narusza to zasadę Otwórz / Zamknij (Otwórz dla rozszerzenia, ale Zamknij dla modyfikacji) i odkryłem, że naruszanie tej zasady, utrzymanie kodu staje się prawdziwy ból, gdy aplikacja rośnie w złożoności. Czemu? Ponieważ reguły domenowe zmieniają się częściej niż chcielibyśmy przyznać, a jeśli reguły sąukryty i osadzony w jednostce takiej jak ta są trudne do przetestowania, trudne do odczytania, trudne do utrzymania, ale prawdziwym powodem, dla którego nie podoba mi się to podejście jest: jeśli zasady walidacji ulegną zmianie, muszę przyjść i edytować moją jednostkę domeny. To był bardzo prosty przykład, ale w RL walidacja może być bardziej złożona

Postępując zgodnie z filozofią Udi Dahana,wyraźne ról, a zalecenie Erica Evansa w niebieskiej książce, następną próbą było zaimplementowanie wzorca specyfikacji, coś takiego

class EmailDomainIsAllowedSpecification : IDomainSpecification<Customer>
{
   private INotAllowedEmailDomainsResolver invalidEmailDomainsResolver;
   public bool IsSatisfiedBy(Customer customer)
   {
      return !this.invalidEmailDomainsResolver.GetInvalidEmailDomains().Contains(customer.Email);
   }
}

Ale potem zdaję sobie sprawę, że aby postępować zgodnie z tym podejściem, musiałem najpierw zmutować moje byty, aby zdaćwartość jest walidowana, w tym przypadku wiadomość e-mail, ale ich zmutowanie spowodowałoby wystrzelenie zdarzeń z mojej domeny, których nie chciałbym robić, dopóki nowy e-mail nie będzie ważny

Więc po rozważeniu tych podejść wyszedłem z tym, ponieważ zamierzam zaimplementować architekturę CQRS:

class EmailDomainIsAllowedValidator : IDomainInvariantValidator<Customer, ChangeEmailCommand>
{
   public void IsValid(Customer entity, ChangeEmailCommand command)
   {
      if(!command.Email.HasValidDomain())  throw new DomainException(“...”);
   }
}

Cóż, to jest główny pomysł, jednostka jest przekazywana do walidatora w przypadku, gdy potrzebujemy jakiejś wartości od jednostki do przeprowadzenia walidacji, komenda zawiera dane pochodzące od użytkownika i ponieważ walidatory są rozważanewstrzykiwalny obiekty mogą mieć wstrzyknięte zewnętrzne zależności, jeśli wymaga tego walidacja.

Teraz dylemat, Jestem zadowolony z takiego projektu, ponieważ moja walidacja jest zamknięta w pojedynczych obiektach, co przynosi wiele korzyści: łatwy test jednostkowy, łatwy w utrzymaniu, niezmienniki domen są wyraźnie wyrażone przy użyciu języka wszechobecnego, łatwe do rozszerzenia, logika walidacji jest scentralizowana i walidatory mogą być używane razem w celu egzekwowania złożonych reguł domeny. I nawet gdy wiem, że umieszczam walidację moich jednostek poza nimi (można argumentować zapach kodu - domena anemiczna), ale myślę, że kompromis jest do zaakceptowania

Ale jest jedna rzecz, której nie zorientowałem się, jak ją wdrożyć w czysty sposób.Jak korzystać z tych komponentów ...

Ponieważ zostaną one wstrzyknięte, nie zmieszczą się naturalnie w moich obiektach domeny, więc zasadniczo widzę dwie opcje:

Przekaż walidatory do każdej metody mojej jednostki

Sprawdź poprawność moich obiektów na zewnątrz (z programu obsługi poleceń)

Nie jestem zadowolony z opcji 1, więc wyjaśniłbym, jak zrobię to za pomocą opcji 2

class ChangeEmailCommandHandler : ICommandHandler<ChangeEmailCommand>
{
   // here I would get the validators required for this command injected
   private IEnumerable<IDomainInvariantValidator> validators;
   public void Execute(ChangeEmailCommand command)
   {
      using (var t = this.unitOfWork.BeginTransaction())
      {
         var customer = this.unitOfWork.Get<Customer>(command.CustomerId);
         // here I would validate them, something like this
         this.validators.ForEach(x =. x.IsValid(customer, command));
         // here I know the command is valid
         // the call to ChangeEmail will fire domain events as needed
         customer.ChangeEmail(command.Email);
         t.Commit();
      }
   }
}

Cóż, to jest to. Czy możesz mi przekazać swoje przemyślenia na ten temat lub podzielić się swoimi doświadczeniami z weryfikacją jednostek domen

EDYTOWAĆ

Myślę, że nie wynika to jasno z mojego pytania, ale prawdziwy problem polega na tym, że: Ukrywanie reguł domeny ma poważne konsekwencje dla przyszłej możliwości utrzymania aplikacji, a także reguły domeny zmieniają się często podczas cyklu życia aplikacji. Dlatego wdrożenie ich w tym celu pozwoli nam je łatwo rozszerzyć. Teraz wyobraź sobie, że w przyszłości zostanie zaimplementowany mechanizm reguł, jeśli reguły zostaną zamknięte poza jednostkami domeny, zmiana ta byłaby łatwiejsza do wdrożenia

Zdaję sobie sprawę, że umieszczenie walidacji poza moimi jednostkami łamie hermetyzację, jak @jgauffin wspomniał w swojej odpowiedzi, ale myślę, że korzyści z umieszczenia walidacji w pojedynczych obiektach są znacznie bardziej istotne niż tylko utrzymanie enkapsulacji jednostki. Teraz myślę, że hermetyzacja ma więcej sensu w tradycyjnej architekturze n-warstwowej, ponieważ encje były używane w kilku miejscach warstwy domeny, ale w architekturze CQRS, gdy nadejdzie polecenie, pojawi się program obsługi komend uzyskujący dostęp do zagregowanego roota i wykonywanie operacji na korzeniu zbiorczym tylko tworząc idealne okno do umieszczenia walidacji.

Chciałbym dokonać niewielkiego porównania korzyści wynikających z umieszczenia walidacji wewnątrz obiektu względem umieszczenia go w poszczególnych obiektach

Walidacja w pojedynczych obiektach

Zawodowiec. Łatwy do napisaniaZawodowiec. Łatwy do testowaniaZawodowiec. Jest to wyraźnie wyrażoneZawodowiec. Staje się częścią projektu domeny, wyrażonego przez obecny wszechobecny językZawodowiec. Ponieważ jest teraz częścią projektu, można go modelować za pomocą diagramów UMLZawodowiec. Niezwykle łatwy w utrzymaniuZawodowiec. Sprawia, że ​​moje jednostki i logika walidacji są luźno powiązaneZawodowiec. Łatwy do rozszerzeniaZawodowiec. Po SRPZawodowiec. Zgodnie z zasadą Open / CloseZawodowiec. Nie łamiesz prawa Demeter (mmm)?Zawodowiec. Jestem scentralizowanyZawodowiec. Może być wielokrotnego użytkuZawodowiec. W razie potrzeby można łatwo wprowadzić zależności zewnętrzneZawodowiec. Jeśli używasz modelu wtyczki, nowe walidatory można dodawać po prostu upuszczając nowe zespoły bez konieczności ponownego kompilowania całej aplikacjiZawodowiec. Wdrożenie silnika reguł byłoby łatwiejszeKon. Łamanie hermetyzacjiKon. Jeśli enkapsulacja jest obowiązkowa, musielibyśmy przekazać poszczególne walidatory do metody encji (agregatu)

Walidacja zamknięta wewnątrz jednostki

Zawodowiec. Zamknięty?Zawodowiec. Wielokrotnego użytku?

Bardzo chciałbym przeczytać twoje przemyślenia na ten temat

questionAnswers(11)

yourAnswerToTheQuestion