Wydaje się, że istnieje powszechna zgoda w społeczności OOP, że konstruktor klasy nie powinien pozostawiać obiektu częściowo, a nawet całkowicie niezainicjowanego.
Co rozumiem przez „inicjalizację”? Z grubsza mówiąc, proces atomowy , który wprowadza nowo utworzony obiekt w stan, w którym utrzymują się wszystkie niezmienniki jego klasy. Powinna być pierwszą rzeczą, która przytrafia się obiektowi (powinna działać tylko raz na obiekt) i nic nie powinno mieć możliwości przechwycenia niezainicjowanego obiektu. (Dlatego częsta rada dotycząca inicjowania obiektu bezpośrednio w konstruktorze klasy. Z tego samego powodu
Initialize
metody są często odrzucane, ponieważ rozbijają one atomowość i umożliwiają uzyskanie i użycie obiektu, który nie jest jeszcze w dobrze zdefiniowanym stanie).
Problem: Kiedy CQRS jest połączone z pozyskiwaniem zdarzeń (CQRS + ES), gdzie wszystkie zmiany stanu obiektu są wychwytywane w uporządkowanej serii zdarzeń (strumień zdarzeń), zastanawiam się, kiedy obiekt faktycznie osiągnie w pełni zainicjowany stan: Na końcu konstruktora klasy, czy po zastosowaniu do obiektu pierwszego zdarzenia?
Uwaga: powstrzymuję się od używania terminu „agreguj root”. Jeśli wolisz, zamień go za każdym razem, gdy czytasz „obiekt”.
Przykład do dyskusji: Załóżmy, że każdy obiekt jest jednoznacznie identyfikowany przez jakąś nieprzezroczystą Id
wartość (pomyśl GUID). Strumień zdarzeń reprezentujący zmiany stanu tego obiektu można zidentyfikować w magazynie zdarzeń na podstawie tej samej Id
wartości: (Nie martwmy się o prawidłową kolejność zdarzeń).
interface IEventStore
{
IEnumerable<IEvent> GetEventsOfObject(Id objectId);
}
Załóżmy ponadto, że istnieją dwa typy obiektów Customer
i ShoppingCart
. Skupmy się na ShoppingCart
: Po utworzeniu koszyki są puste i muszą być powiązane z dokładnie jednym klientem. Ten ostatni bit jest niezmiennikiem klasy: ShoppingCart
Obiekt, który nie jest powiązany z, Customer
jest w niepoprawnym stanie.
W tradycyjnym OOP można modelować to w konstruktorze:
partial class ShoppingCart
{
public Id Id { get; private set; }
public Customer Customer { get; private set; }
public ShoppingCart(Id id, Customer customer)
{
this.Id = id;
this.Customer = customer;
}
}
Brakuje mi jednak sposobu modelowania tego w CQRS + ES bez odroczonej inicjalizacji. Ponieważ ta prosta inicjalizacja jest w rzeczywistości zmianą stanu, czy nie trzeba jej modelować jako zdarzenia ?:
partial class CreatedEmptyShoppingCart
{
public ShoppingCartId { get; private set; }
public CustomerId { get; private set; }
}
// Note: `ShoppingCartId` is not actually required, since that Id must be
// known in advance in order to fetch the event stream from the event store.
To oczywiście musi być pierwsze zdarzenie w ShoppingCart
strumieniu zdarzeń dowolnego obiektu, a obiekt ten zostanie zainicjowany dopiero po zastosowaniu zdarzenia do niego.
Więc jeśli inicjalizacja stanie się częścią „odtwarzania” strumienia zdarzeń (co jest bardzo ogólnym procesem, który prawdopodobnie działałby tak samo, czy to dla Customer
obiektu, ShoppingCart
obiektu czy innego typu obiektu w tym przypadku)…
- Czy konstruktor powinien być pozbawiony parametrów i nic nie robić, pozostawiając całą pracę jakiejś
void Apply(CreatedEmptyShoppingCart)
metodzie (która jest prawie taka sama jak zmarszczone brwiInitialize()
)? - A może konstruktor powinien odbierać strumień zdarzeń i odtwarzać go (co powoduje, że inicjalizacja staje się atomowa, ale oznacza, że konstruktor każdej klasy zawiera tę samą ogólną logikę „odtwarzaj i stosuj”, tj. Niechciane powielanie kodu)?
- A może powinien istnieć zarówno tradycyjny konstruktor OOP (jak pokazano powyżej), który poprawnie inicjuje obiekt, a następnie wszystkie zdarzenia oprócz pierwszego są
void Apply(…)
z nim powiązane?
Nie oczekuję, że odpowiedź zapewni w pełni działającą implementację demonstracyjną; Byłbym bardzo szczęśliwy, gdyby ktoś mógł wyjaśnić, w jaki sposób moje rozumowanie jest wadliwe, lub czy inicjalizacja obiektu naprawdę jest „przeszkodą” w większości implementacji CQRS + ES.
Initialize
które zajmowałyby agregatory konstruktorów (+ być może metoda). To prowadzi mnie do pytania, jak mogłaby wyglądać twoja fabryka?Moim zdaniem, odpowiedź bardziej przypomina twoją sugestię nr 2 dotyczącą istniejących agregatów; dla nowych agregatów bardziej jak nr 3 (ale przetwarzanie zdarzenia zgodnie z sugestią).
Oto kod, mam nadzieję, że to pomoże.
źródło
Cóż, pierwsze zdarzenie powinno polegać na tym, że klient tworzy koszyk , więc kiedy koszyk jest tworzony, masz już identyfikator klienta w ramach wydarzenia.
Jeśli stan utrzymuje się między dwoma różnymi zdarzeniami, z definicji jest to stan poprawny. Jeśli więc powiesz, że prawidłowy koszyk jest powiązany z klientem, oznacza to, że musisz mieć informacje o kliencie podczas tworzenia samego koszyka
źródło
CreatedEmptyShoppingCart
wydarzenie w moim pytaniu: Zawiera informacje o kliencie, tak jak sugerujesz. Moje pytanie dotyczy bardziej tego, jak takie zdarzenie odnosi się do konstruktora klasyShoppingCart
podczas implementacji lub z nim konkuruje .Uwaga: jeśli nie chcesz używać zagregowanego katalogu głównego, „encja” obejmuje większość tego, o co tutaj pytasz, jednocześnie obawiając się o granice transakcji.
Oto inny sposób myślenia o tym: bytem jest tożsamość + stan. W przeciwieństwie do wartości, jednostka jest tym samym facetem, nawet gdy zmienia się jej stan.
Ale sam stan można uznać za obiekt wartości. Rozumiem przez to, że państwo jest niezmienne; historia bytu jest przejściem z jednego niezmiennego stanu do następnego - każde przejście odpowiada zdarzeniu w strumieniu zdarzeń.
Metoda onEvent () jest oczywiście zapytaniem - currentState wcale się nie zmienia, zamiast tego currentState oblicza argumenty użyte do utworzenia nextState.
Zgodnie z tym modelem wszystkie wystąpienia koszyka można uznać za rozpoczynające się od tej samej wartości początkowej ....
Rozdzielenie obaw - ShoppingCart przetwarza polecenie, aby dowiedzieć się, jakie wydarzenie powinno nastąpić; Stan ShoppingCart wie, jak przejść do następnego stanu.
źródło