在OOP社区中似乎已经达成广泛的共识,即类构造函数不应将对象部分或什至完全未初始化。
我所说的“初始化”是什么意思?粗略地讲,原子过程将新创建的对象带入其所有类不变式都成立的状态。它应该是发生在对象上的第一件事(每个对象只能运行一次),并且任何内容都不应该拥有未初始化的对象。(因此,经常建议在类构造函数中执行对象初始化。出于同样的原因,
Initialize
方法常常被皱眉,因为这些方法打破了原子性并使得可以持有和使用尚未使用的对象成为可能。处于定义良好的状态。)
问题:当CQRS与事件源(CQRS + ES)结合使用时,对象的所有状态变化都被捕获在一系列有序的事件(事件流)中,我想知道对象何时真正达到完全初始化的状态:在类构造函数的末尾,还是在将第一个事件应用于对象之后?
注意:我避免使用“聚合根”一词。如果愿意,在阅读“对象”时将其替换。
讨论示例:假定每个对象由某个不透明Id
值唯一标识(请考虑GUID)。可以使用相同的Id
值在事件存储中标识表示该对象状态变化的事件流:(不用担心正确的事件顺序。)
interface IEventStore
{
IEnumerable<IEvent> GetEventsOfObject(Id objectId);
}
进一步假设有两种对象类型Customer
和ShoppingCart
。让我们关注ShoppingCart
:创建时,购物车为空,必须与一个客户完全关联。最后一点是类不变式:ShoppingCart
与a无关的对象Customer
处于无效状态。
在传统的OOP中,可以在构造函数中对此建模:
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
方法)相同的位置。这使我想到一个问题,您这样的工厂会是什么样?