Похоже, что в сообществе ООП широко распространено мнение, что конструктор класса не должен оставлять объект частично или даже полностью неинициализированным.
Что я имею в виду под «инициализацией»? Грубо говоря, атомарный процесс, который приводит вновь созданный объект в состояние, в котором содержатся все его классовые инварианты. Это должно быть первое, что происходит с объектом (он должен запускаться только один раз для каждого объекта), и ничто не должно иметь права завладеть неинициализированным объектом. (Таким образом, часто советуют выполнять инициализацию объекта прямо в конструкторе класса. По той же причине
Initialize
методы часто не одобряются, поскольку они разбивают атомарность и позволяют получить и использовать объект, который еще не создан. в четко определенном состоянии.)
Проблема: Когда CQRS объединяется с источником событий (CQRS + ES), где все изменения состояния объекта попадают в упорядоченную серию событий (поток событий), мне остается только удивляться, когда объект фактически достигает полностью инициализированного состояния: В конце конструктора класса или после того, как к объекту было применено самое первое событие?
Примечание: я воздерживаюсь от использования термина «совокупный корень». Если вы предпочитаете, заменяйте его всякий раз, когда читаете «объект».
Пример для обсуждения: предположим, что каждый объект уникально идентифицируется по некоторому непрозрачному Id
значению (например, GUID). Поток событий, представляющий изменения состояния этого объекта, может быть идентифицирован в хранилище событий по тому же Id
значению: (Давайте не будем беспокоиться о правильном порядке событий.)
interface IEventStore
{
IEnumerable<IEvent> GetEventsOfObject(Id objectId);
}
Предположим далее, что есть два типа объектов Customer
и ShoppingCart
. Давайте сосредоточимся на том, что ShoppingCart
: корзины для покупок пусты и должны быть связаны только с одним клиентом. Этот последний бит является инвариантом класса: ShoppingCart
объект, который не связан с, Customer
находится в недопустимом состоянии.
В традиционном ООП это можно смоделировать в конструкторе:
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;
}
}
Однако я не знаю, как смоделировать это в CQRS + ES, не заканчивая отложенной инициализацией. Поскольку этот простой бит инициализации фактически является изменением состояния, не нужно ли его моделировать как событие ?:
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.
Очевидно, это должно быть самое первое событие в ShoppingCart
потоке событий любого объекта, и этот объект будет инициализирован только после того, как событие будет применено к нему.
Поэтому, если инициализация становится частью потока воспроизведения «Воспроизведение» (это очень общий процесс, который, вероятно, будет работать одинаково, будь то для Customer
объекта или ShoppingCart
объекта или любого другого типа объекта в этом отношении)…
- Должен ли конструктор быть без параметров и ничего не делать, оставляя всю работу какому-либо
void Apply(CreatedEmptyShoppingCart)
методу (который почти такой же, как нахмурившийсяInitialize()
)? - Или же конструктор должен принять поток событий и воспроизвести его (что делает инициализацию снова атомарной, но означает, что каждый конструктор класса содержит одну и ту же универсальную логику «воспроизведения и применения», то есть нежелательное дублирование кода)?
- Или должен существовать традиционный конструктор ООП (как показано выше), который правильно инициализирует объект, а затем все события, кроме первого,
void Apply(…)
связаны с ним?
Я не ожидаю ответа, чтобы обеспечить полностью работающую демонстрационную реализацию; Я уже был бы очень рад, если бы кто-то мог объяснить, где мои рассуждения ошибочны, или действительно ли инициализация объекта является «болевым пунктом» в большинстве реализаций CQRS + ES.
Initialize
занимали бы конструкторы совокупности (+ возможно, метод). Это подводит меня к вопросу, на что может быть похожа такая ваша фабрика?На мой взгляд, я думаю, что ответ больше похож на ваше предложение № 2 для существующих агрегатов; для новых агрегатов больше похоже на # 3 (но обрабатывает событие, как вы предложили).
Вот код, надеюсь, он поможет.
источник
Что ж, первое событие должно состоять в том, что клиент создает корзину покупок , поэтому, когда корзина покупок создана, у вас уже есть идентификатор клиента как часть события.
Если состояние выполняется между двумя различными событиями, по определению это допустимое состояние. Таким образом, если вы говорите, что с покупателем связана действительная корзина, это означает, что вам нужно иметь информацию о покупателе при создании самой корзины.
источник
CreatedEmptyShoppingCart
событие в моем вопросе: там есть информация о клиенте, как вы предлагаете. Мой вопрос больше о том, как такое событие связано или конкурирует с конструктором классаShoppingCart
во время реализации.Примечание: если вы не хотите использовать агрегатный корень, «сущность» охватывает большую часть того, о чем вы здесь спрашиваете, в то же время не обращая внимания на границы транзакций.
Вот еще один способ думать об этом: сущность - это личность + состояние. В отличие от значения, сущность - это тот же парень, даже когда его состояние меняется.
Но само государство можно рассматривать как объект стоимости. Я имею в виду, что государство неизменно; история объекта - это переход из одного неизменного состояния в другое - каждый переход соответствует событию в потоке событий.
Конечно, метод onEvent () является запросом - currentState вообще не изменяется, вместо этого currentState вычисляет аргументы, используемые для создания nextState.
Следуя этой модели, можно считать, что все экземпляры корзины покупок начинаются с одинакового начального значения
Разделение проблем - ShoppingCart обрабатывает команду, чтобы выяснить, какое событие должно быть следующим; Штат ShoppingCart знает, как добраться до следующего штата.
источник