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 Car
używa dwóch komponentów:
Transmission
,Engine
.
Zarówno Transmission
i Engine
obiekty wartości są reprezentowane Super typy i mają według rodzajów sub, Automatic
i Manual
przekładnie, albo Petrol
i Electric
silniki odpowiednio.
W tej dziedzinie, żyjący we własnym pomyślnie utworzony Transmission
, czy to Automatic
lub Manual
, lub też type Engine
jest całkowicie w porządku. Ale Car
agregacja wprowadza kilka nowych reguł, mających zastosowanie tylko wtedy, gdy Transmission
i Engine
obiekty są używane w tym samym kontekście. Mianowicie:
- Gdy samochód korzysta z
Electric
silnika, jedynym dozwolonym typem skrzyni biegów jestAutomatic
. - Gdy samochód korzysta z
Petrol
silnika, może mieć dowolny z nichTransmission
.
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ć CreateCar
sprawdzają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ą CreateUser
polecenia.
Polecenie zawiera Id
uż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 UserRepository
jako 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 User
agregacji, a weryfikator poleceń powinien sprawdzać tylko unikalność, a jeśli sprawdzanie poprawności się powiedzie, powinienem przystąpić do tworzenia User
agregacji w CreateUserCommandHandler
i 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 UserRepository
z User
obiektu 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ń.
CommandDispatcher
.Odpowiedzi:
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ń).
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:
isValid
metody, 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.command validators
klasy, 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 validators
moż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ń.When a car uses Electric engine the only allowed transmission type is Automatic
należy sprawdzić tutaj.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,
CommandDispatcher
wię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.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 wHTTP 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ń:
źródło
EmailAddress
obiekt wartości, który się sam sprawdza.EmailAddress
w 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ą.EmailAddress
jest wygodnym przykładem, ponieważ cała koncepcja tej wartości ma globalne wymagania walidacyjne.UserCanPlaceOrdersOnlyIfHeIsNotLockedValidator
. Widać, że jest to osobna domena dla Zamówień, więc nie może być zweryfikowana przez samą OrderAggregate.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
ChangeEmail
polecenie, możemy doskonale zilustrować, dlaczego nie chcesz logiki biznesowej w infrastrukturze poleceń, ponieważ musisz powielić swoje reguły: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
User
agregatu, 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 dotyczyUser
, ale tak naprawdę nie jest. „Unikalność” wiadomości e-mail dotyczy zbioruUsers
(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ęciUsers
) 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ćUserEmailAlreadyExists
wyjątek). Alternatywnie, domenęUserService
można przypisać do tworzenia nowychUsers
i 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.
źródło