Jak dokładnie należy zweryfikować Polecenie CQRS i przekształcić je w obiekt domeny?

22

Od dłuższego czasu dostosowuję CQRS 1 dla biedaka, ponieważ uwielbiam jego elastyczność polegającą na posiadaniu szczegółowych danych w jednym magazynie danych, zapewniając duże możliwości analizy, a tym samym zwiększając wartość biznesową, aw razie potrzeby inny dla odczytów zawierających dane zdormalizowane w celu zwiększenia wydajności .

Ale niestety właściwie od samego początku zmagałem się z problemem, w którym dokładnie powinienem umieścić logikę biznesową w tego typu architekturze.

Z tego, co rozumiem, polecenie jest środkiem do komunikowania zamiarów i samo w sobie nie ma powiązań z domeną. Są to w zasadzie transfer danych (głupi - jeśli chcesz). Ma to na celu ułatwienie przenoszenia poleceń między różnymi technologiami. To samo dotyczy zdarzeń, co odpowiedzi na pomyślnie zakończone zdarzenia.

W typowej aplikacji DDD logika biznesowa znajduje się w jednostkach, obiektach wartości, zagregowanych korzeniach, są one bogate zarówno w dane, jak i zachowanie. Ale polecenie nie jest przedmiotem domeny, dlatego nie powinno być ograniczone do reprezentacji danych w domenie, ponieważ powoduje to zbyt duże obciążenie.

Tak więc prawdziwe pytanie brzmi: gdzie dokładnie jest logika?

Dowiedziałem się, że najczęściej spotykam się z tą walką, próbując zbudować dość skomplikowany agregat, który określa pewne zasady dotyczące kombinacji jego wartości. Ponadto, podczas modelowania obiektów domeny lubię podążać za paradygmatem szybkiego działania , wiedząc, że kiedy obiekt osiągnie metodę, jest w prawidłowym stanie.

Załóżmy, że agregat Carużywa dwóch komponentów:

  • Transmission,
  • Engine.

Zarówno Transmissioni Engineobiekty wartości są reprezentowane Super typy i mają według rodzajów sub, Automatici Manualprzekładnie, albo Petroli Electricsilniki odpowiednio.

W tej dziedzinie, żyjący we własnym pomyślnie utworzony Transmission, czy to Automaticlub Manual, lub też type Enginejest całkowicie w porządku. Ale Caragregacja wprowadza kilka nowych reguł, mających zastosowanie tylko wtedy, gdy Transmissioni Engineobiekty są używane w tym samym kontekście. Mianowicie:

  • Gdy samochód korzysta z Electricsilnika, jedynym dozwolonym typem skrzyni biegów jest Automatic.
  • Gdy samochód korzysta z Petrolsilnika, może mieć dowolny z nich Transmission.

Mogłem wychwycić naruszenie tej kombinacji składników na poziomie tworzenia polecenia, ale jak już wcześniej powiedziałem, z tego, co rozumiem, nie powinno się to robić, ponieważ polecenie zawierałoby logikę biznesową, która powinna być ograniczona do warstwy domeny.

Jedną z opcji jest przeniesienie sprawdzania poprawności logiki biznesowej do samego sprawdzania poprawności poleceń, ale nie wydaje się to również właściwe. Wydaje mi się, że dekonstruowałbym polecenie, sprawdzając jego właściwości pobrane za pomocą metod pobierających i porównując je w ramach sprawdzania poprawności oraz sprawdzając wyniki. To krzyczy jak naruszenie prawa Demeter .

Odrzucając wspomnianą opcję sprawdzania poprawności, ponieważ nie wydaje się ona wykonalna, wydaje się, że należy użyć polecenia i zbudować z niego agregat. Ale gdzie powinna istnieć ta logika? Czy powinien to być program obsługi poleceń odpowiedzialny za obsługę konkretnego polecenia? A może powinien być w ramach sprawdzania poprawności poleceń (nie podoba mi się również to podejście)?

Obecnie używam polecenia i tworzę z niego agregację w ramach odpowiedzialnego modułu obsługi poleceń. Ale gdy to zrobię, powinienem mieć CreateCarsprawdzający poprawność polecenia, który w ogóle nie zawierałby niczego, ponieważ gdyby polecenie istniało, wówczas zawierałoby składniki, które, jak wiem, są poprawne w oddzielnych przypadkach, ale agregacja może powiedzieć inaczej.


Wyobraźmy sobie inny scenariusz łączący różne procesy sprawdzania poprawności - tworzenie nowego użytkownika za pomocą CreateUserpolecenia.

Polecenie zawiera Idużytkownika, który zostanie utworzony i ich Email.

System określa następujące zasady dotyczące adresu e-mail użytkownika:

  • musi być unikalny,
  • nie może być pusty
  • musi mieć maksymalnie 100 znaków (maksymalna długość kolumny db).

W takim przypadku, mimo że unikalny adres e-mail jest regułą biznesową, sprawdzanie go w agregacji nie ma większego sensu, ponieważ musiałbym załadować cały zestaw bieżących wiadomości e-mail w systemie do pamięci i sprawdzić adres e-mail w poleceniu przeciwko agregatowi ( Eeeek! Coś, coś, wydajność.). Z tego powodu przeniósłbym to sprawdzenie do sprawdzania poprawności polecenia, które wziąłoby UserRepositoryjako zależność i użyłoby repozytorium do sprawdzenia, czy użytkownik z e-mailem obecnym w poleceniu już istnieje.

Jeśli chodzi o to, nagle sensowne jest umieszczenie dwóch pozostałych reguł e-mail w walidatorze poleceń. Ale mam wrażenie, że reguły powinny być naprawdę obecne w Useragregacji, a weryfikator poleceń powinien sprawdzać tylko unikalność, a jeśli sprawdzanie poprawności się powiedzie, powinienem przystąpić do tworzenia Useragregacji w CreateUserCommandHandleri przekazywania jej do repozytorium, które ma zostać zapisane.

Wydaje mi się, że tak, ponieważ metoda zapisu w repozytorium prawdopodobnie zaakceptuje agregację, która gwarantuje, że po przekazaniu agregacji wszystkie niezmienniki zostaną spełnione. Kiedy logika (np non-pustki) występuje tylko w ramach walidacji polecenia samego inny programista mógłby całkowicie pominąć ten walidacji i nazywają Zapisz metoda w UserRepositoryz Userobiektu bezpośrednio, które mogłyby prowadzić do śmiertelnego błędu bazy danych, ponieważ e-mail może mieć trwało zbyt długo.

Jak osobiście radzisz sobie z tymi złożonymi walidacjami i transformacjami? Jestem najbardziej zadowolony z mojego rozwiązania, ale czuję, że potrzebuję potwierdzenia, że ​​moje pomysły i podejścia nie są całkowicie głupie, aby być całkiem zadowolonym z wyborów. Jestem całkowicie otwarty na zupełnie inne podejścia. Jeśli masz coś, co osobiście wypróbowałeś i działało dla ciebie bardzo dobrze, chciałbym zobaczyć twoje rozwiązanie.


1 Pracując jako programista PHP odpowiedzialny za tworzenie systemów RESTful, moja interpretacja CQRS odbiega nieco od standardowego podejścia do przetwarzania poleceń asynchronicznych , na przykład czasami zwraca wyniki poleceń z powodu potrzeby synchronicznego przetwarzania poleceń.

Andy
źródło
potrzebuję przykładowego kodu. jak wyglądają twoje obiekty poleceń i gdzie je tworzysz?
Ewan
@Ewan Dodam próbki kodu później albo dziś, albo jutro. Wyjazd na wycieczkę za kilka minut.
Andy
Jako programista PHP proponuję rzucić okiem na moją implementację CQRS + ES: github.com/xprt64/cqrs-es
Constantin Galbenu
@ ConstantinGALBENU Jeśli uznamy interpretację CQRS przez Grega Younga za słuszną (co prawdopodobnie powinniśmy), to twoje rozumienie CQRS jest błędne - a przynajmniej implementacja PHP. Polecenia nie są obsługiwane przez agregaty bezpośrednio. Polecenia mają być obsługiwane przez procedury obsługi poleceń, które mogą powodować zmiany w agregacjach, które następnie generują zdarzenia, które zostaną wykorzystane do replikacji stanu.
Andy,
Nie sądzę, aby nasze interpretacje były inne. Musisz po prostu wykopać więcej w DDD (na taktycznym poziomie agregatów) lub otworzyć oczy szerzej. Istnieją co najmniej dwa style wdrażania CQRS. Używam jednego z nich. Moja implementacja bardziej przypomina model Actora i sprawia, że ​​warstwa aplikacji jest bardzo cienka, co zawsze jest dobrą rzeczą. Zauważyłem, że w tych aplikacjach jest dużo duplikacji kodu i postanowiłem je zastąpić CommandDispatcher.
Constantin Galbenu,

Odpowiedzi:

22

Poniższa odpowiedź jest w kontekście stylu CQRS promowanego przez cqrs.nu, w którym polecenia docierają bezpośrednio do agregatów. W tym stylu architektonicznym usługi aplikacji są zastępowane przez komponent infrastruktury ( CommandDispatcher ), który identyfikuje agregację, ładuje ją, wysyła komendę, a następnie utrwala agregację (jako serię zdarzeń, jeśli używane jest pozyskiwanie zdarzeń).

Tak więc prawdziwe pytanie brzmi: gdzie dokładnie jest logika?

Istnieje wiele rodzajów logiki (sprawdzania poprawności). Ogólnym pomysłem jest jak najszybsze wykonanie logiki - jeśli chcesz, możesz szybko zawieść. Tak więc sytuacje są następujące:

  • struktura samego obiektu polecenia; konstruktor polecenia ma pewne wymagane pola, które muszą być obecne, aby polecenie mogło zostać utworzone; jest to pierwsza i najszybsza walidacja; jest to oczywiście zawarte w poleceniu.
  • sprawdzanie poprawności pól na niskim poziomie, takie jak brak pustki w niektórych polach (np. nazwa użytkownika) lub format (prawidłowy adres e-mail). Ten rodzaj sprawdzania poprawności powinien być zawarty w samym poleceniu, w konstruktorze. Istnieje inny styl posiadania isValidmetody, ale wydaje mi się to bezcelowe, ponieważ ktoś musiałby pamiętać o wywołaniu tej metody, gdy w rzeczywistości wystarczające byłoby wykonanie instancji polecenia.
  • osobne command validatorsklasy, które mają obowiązek zweryfikować polecenie. Używam tego rodzaju sprawdzania poprawności, gdy muszę sprawdzić informacje z wielu agregatów lub źródeł zewnętrznych. Możesz użyć tego, aby sprawdzić unikalność nazwy użytkownika. Command validatorsmoże mieć zastrzyk jakichkolwiek zależności, takich jak repozytoria. Należy pamiętać, że ta weryfikacja jest ostatecznie spójna z agregacją (tzn. Kiedy użytkownik zostanie utworzony, w międzyczasie można utworzyć innego użytkownika o tej samej nazwie)! Nie próbuj też umieszczać tutaj logiki, która powinna znajdować się w agregacie! Walidatory poleceń różnią się od menedżerów Sagas / Process, które generują polecenia na podstawie zdarzeń.
  • metody agregujące, które odbierają i przetwarzają polecenia. Jest to ostatnia (rodzaj) walidacji, która ma miejsce. Agregacja wyodrębnia dane z polecenia i przy użyciu pewnej podstawowej logiki biznesowej, którą akceptuje (wykonuje zmiany stanu) lub odrzuca je. Ta logika jest sprawdzana w bardzo spójny sposób. To ostatnia linia obrony. W twoim przykładzie regułę When a car uses Electric engine the only allowed transmission type is Automaticnależy sprawdzić tutaj.

Wydaje mi się, że tak, ponieważ metoda zapisu w repozytorium prawdopodobnie zaakceptuje agregację, która gwarantuje, że po przekazaniu agregacji wszystkie niezmienniki zostaną spełnione. Gdy logika (np. Brak pustki) występuje tylko w samym sprawdzaniu poprawności polecenia, inny programista może całkowicie pominąć to sprawdzanie i wywołać metodę zapisywania w UserRepository z obiektem użytkownika bezpośrednio, co może prowadzić do krytycznego błędu bazy danych, ponieważ wiadomość e-mail mogło być za długo.

Używając powyższych technik, nikt nie może tworzyć niepoprawnych poleceń ani omijać logiki wewnątrz agregatów. Walidatory poleceń są automatycznie ładowane + wywoływane przez, CommandDispatcherwięc nikt nie może wysłać polecenia bezpośrednio do agregatu. Można wywołać metodę na agregacie przekazując polecenie, ale nie można zachować zmian, więc byłoby to bezcelowe / nieszkodliwe.

Pracując jako programista PHP odpowiedzialny za tworzenie systemów RESTful, moja interpretacja CQRS odbiega nieco od standardowego podejścia do przetwarzania poleceń asynchronicznych, na przykład czasami zwraca wyniki poleceń z powodu potrzeby synchronicznego przetwarzania poleceń.

Jestem również programistą PHP i nie zwracam niczego z moich programów obsługi poleceń (metody agregujące w formularzu handleSomeCommand). Jednak dość często zwracam informacje do klienta / przeglądarki w HTTP response, na przykład, identyfikator nowo utworzonego zagregowanego katalogu głównego lub coś z modelu odczytu, ale nigdy nie zwracam (naprawdę nigdy ) niczego z moich metod zagregowanego polecenia. Prosty fakt, że polecenie zostało zaakceptowane (i przetworzone - mówimy o synchronicznym przetwarzaniu PHP, prawda ?!) jest wystarczające.

Zwracamy coś do przeglądarki (i nadal wykonujemy CQRS według książki), ponieważ CQRS nie jest architekturą wysokiego poziomu .

Przykład działania walidatorów poleceń:

Ścieżka polecenia przez walidatory poleceń w drodze do agregatu

Constantin Galbenu
źródło
Jeśli chodzi o twoją strategię walidacji, punkt drugi wyskakuje na mnie jako prawdopodobne miejsce, w którym logika będzie często powtarzana. Z pewnością ktoś chciałby, aby agregacja użytkownika zweryfikowała niepusty i dobrze sformatowany e-mail, a także nie? Staje się to widoczne po wprowadzeniu polecenia ChangeEmail.
King-side-slide
@ King-side-slide nie, jeśli masz EmailAddressobiekt wartości, który się sam sprawdza.
Constantin Galbenu
To jest całkowicie poprawne. Można enkapsulować EmailAddressw celu ograniczenia powielania. Co ważniejsze, robiąc to, przenosisz logikę z komendy do swojej domeny. Warto zauważyć, że można to zrobić za daleko. Często podobne elementy wiedzy (obiekty wartości) mogą mieć różne wymagania dotyczące walidacji, w zależności od tego, czyje z nich korzystają. EmailAddressjest wygodnym przykładem, ponieważ cała koncepcja tej wartości ma globalne wymagania walidacyjne.
King-side-slide
Podobnie idea „walidatora poleceń” wydaje się niepotrzebna. Celem nie jest zapobieganie tworzeniu i wysyłaniu nieprawidłowych poleceń. Celem jest uniemożliwienie im wykonania. Na przykład mogę przekazać dowolne dane za pomocą adresu URL. Jeśli jest nieważny, system odrzuca moją prośbę. Polecenie jest nadal tworzone i wysyłane. Jeśli polecenie wymaga wielu agregatów do sprawdzenia poprawności (tj. Zbioru użytkowników w celu sprawdzenia unikalności wiadomości e-mail), usługa domenowa jest lepszym rozwiązaniem. Obiekty takie jak „x validator” są często znakiem modelu anemicznego, w którym dane są oddzielane od zachowania.
King-side-slide
1
@ king-side-slide Konkretnym przykładem jest UserCanPlaceOrdersOnlyIfHeIsNotLockedValidator. Widać, że jest to osobna domena dla Zamówień, więc nie może być zweryfikowana przez samą OrderAggregate.
Constantin Galbenu
6

Jedną z fundamentalnych przesłanek DDD jest to, że modele domen sprawdzają się. Jest to krytyczna koncepcja, ponieważ podnosi Twoją domenę jako podmiot odpowiedzialny za zapewnienie przestrzegania reguł biznesowych. Utrzymuje również model domeny jako centrum rozwoju.

System CQRS (jak słusznie zauważyłeś) to szczegół implementacji reprezentujący ogólną subdomenę, która implementuje swój własny spójny mechanizm. Twój model nie powinien w żaden sposób polegać na jakiejkolwiek części infrastruktury CQRS, aby zachowywać się zgodnie z regułami biznesowymi. Celem DDD jest modelowanie zachowania systemu w taki sposób, aby wynik był użyteczną abstrakcją wymagań funkcjonalnych Twojej głównej domeny biznesowej. Usunięcie dowolnego elementu tego zachowania z modelu, jakkolwiek kuszące, zmniejsza integralność i spójność modelu (i czyni go mniej użytecznym).

Po prostu rozszerzając przykład o ChangeEmailpolecenie, możemy doskonale zilustrować, dlaczego nie chcesz logiki biznesowej w infrastrukturze poleceń, ponieważ musisz powielić swoje reguły:

  • adres e-mail nie może być pusty
  • adres e-mail nie może być dłuższy niż 100 znaków
  • adres e-mail musi być unikalny

Teraz, gdy możemy być pewni, że nasza logika musi należeć do naszej domeny, rozwiążmy kwestię „gdzie”. Dwie pierwsze reguły można łatwo zastosować do naszego Useragregatu, ale ta ostatnia reguła jest nieco bardziej szczegółowa; taki, który wymaga pogłębienia wiedzy, aby uzyskać głębszy wgląd. Na pierwszy rzut oka może się wydawać, że ta zasada dotyczy User, ale tak naprawdę nie jest. „Unikalność” wiadomości e-mail dotyczy zbioru Users(w zależności od zakresu).

Ach ha! Mając to na uwadze, staje się zupełnie jasne, że twoja UserRepository(twoja kolekcja w pamięci Users) może być lepszym kandydatem do egzekwowania tego niezmiennika. Metoda „zapisz” jest prawdopodobnie najbardziej rozsądnym miejscem na włączenie czeku (w którym można rzucić UserEmailAlreadyExistswyjątek). Alternatywnie, domenę UserServicemożna przypisać do tworzenia nowych Usersi aktualizowania ich atrybutów.

Szybka awaria jest dobrym podejściem, ale można to zrobić tylko tam, gdzie i kiedy pasuje do reszty modelu. Niezwykle kuszące może być sprawdzenie parametrów w metodzie usługi aplikacji (lub komendzie) przed dalszym przetwarzaniem w celu wychwycenia błędów, gdy (deweloper) wiesz, że wywołanie zakończy się niepowodzeniem gdzieś głębiej. Ale robiąc to, powieliłeś (i wyciekłeś) wiedzę w sposób, który prawdopodobnie będzie wymagał więcej niż jednej aktualizacji kodu, gdy zmienią się reguły biznesowe.

zjeżdżalnia królewska
źródło
2
Zgadzam się z tym. Moje czytanie do tej pory (bez CQRS) mówi mi, że walidacja powinna zawsze odbywać się w modelu domeny, aby chronić niezmienniki. Teraz czytam CQRS, który mówi mi, żebym sprawdził poprawność w obiektach Command. To wydaje się sprzeczne z intuicją. Czy znasz jakieś przykłady, np. Na GitHub, gdzie sprawdzanie poprawności jest umieszczane w modelu domeny zamiast polecenia? +1.
w0051977