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
.