У спільноті OOP, як видається, існує широка думка, що конструктор класів не повинен залишати об'єкт частково або навіть повністю неініціалізованим.
Що я маю на увазі під "ініціалізацією"? Грубо кажучи, атомний процес, який приводить новостворений об'єкт у стан, у якому тримаються всі його класові інваріанти. Це має бути першим, що трапляється з об'єктом, (він повинен запускатися лише один раз на об'єкт), і нічого не повинно бути дозволено, щоб дістатись неініціалізованого об'єкта. (Таким чином, часті поради щодо виконання права ініціалізації об'єкта в конструкторі класу. З тієї ж причини
Initialize
методи часто нахмурюються, оскільки вони розбивають атомність і дають можливість отримати і використовувати об'єкт, який ще не існує. у чітко визначеному стані.)
Проблема: Коли CQRS поєднується з джерелом подій (CQRS + ES), де всі зміни стану об'єкта потрапляють у впорядковану серію подій (потік подій), мені залишається цікаво, коли об’єкт насправді досягає повністю ініціалізованого стану: Наприкінці конструктора класу чи після того, як до об'єкта було застосовано першу подію?
Примітка. Я утримуюсь від використання терміна "сукупний корінь". Якщо ви віддаєте перевагу, замінюйте його щоразу, коли читаєте "об'єкт".
Приклад для обговорення: Припустимо, що кожен об'єкт однозначно ідентифікується деяким непрозорим Id
значенням (думаю GUID). Потік подій, що представляє зміни стану цього об'єкта, може бути ідентифікований у сховищі подій за тим самим Id
значенням: (Не будемо турбуватися про правильний порядок подій.)
interface IEventStore
{
IEnumerable<IEvent> GetEventsOfObject(Id objectId);
}
Припустимо, що існує два типи об'єктів Customer
і ShoppingCart
. Давайте зупинимось на наступному ShoppingCart
: Коли створені, кошики для покупки порожні, їх потрібно асоціювати саме з одним клієнтом. Цей останній біт - інваріант класу: ShoppingCart
Об'єкт, який не асоційований з a, 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()
)? - Чи повинен конструктор отримувати потік події та відтворювати його (що робить ініціалізацію знову атомною, але означає, що кожен конструктор класу містить однакову загальну логіку «відтворення та застосування», тобто небажане дублювання коду)?
- Чи повинен бути як традиційний конструктор OOP (як показано вище), який належним чином ініціалізує об'єкт, і тоді всі події, окрім першої, -
void Apply(…)
це до нього?
Я не сподіваюсь на відповідь, щоб забезпечити повністю працюючу демо-версію; Я вже буду дуже радий, якби хтось міг пояснити, де моє міркування хибно, чи ініціалізація об'єкта насправді є "больовою точкою" у більшості реалізацій CQRS + ES.
Initialize
б займали агрегативні конструктори (+ можливо метод). Це призводить мене до питання, як може виглядати така ваша фабрика?