我正在尝试弄清楚如何使用CQRS/ES方法处理复杂的域模型。让我们想象我们有例如订单域实体,该实体处理秩序的状态和行为。它具有带有过渡规则的状态属性,用于在状态之间切换(实现状态模式或任何其他类型的状态机器(。根据DDD原则,该逻辑应以类别(代表顺序模型(本身的方式实现,具有approve()
,cancel()
,ship()
等方法。
查看这种类型体系结构的不同公共示例,事实证明,域实体和总体根源是相同的,并且可以处理状态和行为,甚至可以从事件中进行投射。这不是违反SRP吗?
但是我的问题更具体:如果我想处理新命令(并应用新事件(,是否应该从事件流重新构建实体(即从写入模型和写入db(并调用其行为方法(将事件应用于状态(处理业务规则?或者只是本身处理命令和事件,而没有任何写作模型实体?
伪码说明:
class ApproveOrderHandler
{
private EventStore eventStore
// ...
public void handle(ApproveOrder event)
{
Order order = this.eventStore.findById(event.getOrderId()); // getting order projection from event store
order.approve(); // handling business logic
this.eventStore.save(order.releaseEvents()); // save new events (OrderApproved)
}
}
class Order extends AbstractAggregate
{
private Uuid id;
private DateTime takenAt;
private OrderStatus status;
// ...
public void approve()
{
this.status.approve(); // business rules blah blah
this.Apply(new OrderApproved(this.id)); // applying event
}
// ...
}
不是过度的吗?
我应该如何处理事件源中实体之间的关系?如果它们仅存在于"读取模型"中,则域实体类中没有意义。
编辑:或者也许我应该将状态快照存储在"读取数据库"中并从中恢复实体?但这打破了"阅读不同模型"的想法...
edit2:固定的读/写模型错误
tl; dr
但是我的问题更具体:如果我想处理新命令(并应用新事件(,是否应该从事件流重新构建实体(即从写入模型和写入db(并调用其行为方法(将事件应用于状态(处理业务规则?
是。
或只是处理命令和事件本身,而没有任何写模式实体?
no。
再次有感觉
命令处理程序生活在应用程序组件中;业务模型生活在域组件中。
保持这些组件分开的动机:使模型更换成本有效。域专家关心的是企业获得其 win 的是域模型。我们不会期望撰写一次业务模型并在所有时间内纠正它 - 我们更有可能了解有关该模型如何工作的更多信息,因此定期对模型进行改进。因此,重要的是,没有很多阻力将模型的一个版本替换为另一个版本 - 我们希望替换很容易。我们希望将更改反映在我们获得的业务价值中所需的工作量。
所以我们希望好东西与"管道"分开。
将所有业务逻辑保留在域组件中,可为您带来两个简单的胜利;首先,您永远不必猜测业务逻辑的生活在哪里 - 用例的细节很容易或硬,业务逻辑将按顺序而不是其他任何地方。其次,由于业务逻辑不在命令处理程序中,因此您不必担心创建一堆测试双打以满足这些依赖性要求 - 您可以直接针对域模型进行测试。
因此,我们使用处理程序来重建实体并调用其业务逻辑方法,而不是处理业务逻辑本身?
几乎 - 我们使用存储库来重建实体并汇总以处理业务逻辑。命令处理程序的角色是编排;这是 data 模型与 domain 模型之间的胶水。
查看这种类型体系结构的不同公共示例,事实证明,域实体和总体根源是相同的,并且可以处理状态和行为,甚至可以从事件中进行投射。这不是违反SRP吗?
不,没有。"责任"是一个模糊的术语,但在这种情况下,意味着"改变的理由",而总体根源只有一种(一种(改变的理由:业务需求改变。不影响聚集根的更改理由的一个例子是基础架构的更改,即您更改为事件存储从MySql
更改为CC_4。
但是我的问题更具体:如果我想处理新命令(并应用新事件(,是否应该从事件流重新构建实体(即从写入模型和写入db(并调用其行为方法(将事件应用于状态(处理业务规则?
每次命令都达到一个汇总,该汇总实例是从其事件流重构(从Event store
加载的 - 写入侧持续性(,通过在命令它们是生成的;可以进行优化作为快照,但应避免使用,直到有必要证明为止。
或只是处理命令和事件本身,而没有任何写模式实体?
您需要拥有一个写模型实体,又称汇总;该模型通过拒绝与先前生成的事件的不兼容的命令来执行业务规则。
您的伪代码应该看起来像这样:
class ApproveOrderHandler
{
private EventStore eventStore
// ...
public void handle(ApproveOrder event)
{
Order order = this.eventStore.findById(event.getOrderId()); // getting order projection from event store
order.approve(); // handling business logic
this.eventStore.save(order.releaseEvents()); // save new events (OrderApproved)
}
}
class Order extends AbstractAggregate
{
private Uuid id;
private DateTime takenAt;
private OrderStatus status;
// ...
public void approve()
{
if(!this.canBeApproved){ //here is a business rule enforced!
throw new Exception('Order cannot be approved');
}
if(this.status.isAlreadyApproved()){
return; //idempotent operation
}
// this line of code was moved to its own Apply method
this.generateAndApplyEvent(new OrderApproved(this.id)); // applying event
}
//this method is called in two situations: when the aggregate is reconstructed from the eventstream and when the event is raised for the first time
public void Apply(OrderApproved event)
{
this.status.approve(); // transition change
}
// ...
}
不是过度的吗?
不,不是。请注意,我移动了正在更改订单状态的代码行
我应该如何处理事件源中实体之间的关系?如果它们仅存在于"读取模型"中,则域实体类中没有意义。
在写入模型中也存在实体之间(聚集根之间(之间的关系,但引用仅由ID
。
编辑:或者也许我应该将状态快照存储在"读取数据库"中并从中恢复实体?但这打破了"阅读不同模型"的想法...
聚合快照(当激活/使用时(通常沿事件流中存储在事件提交中(事件提交由单个命令执行生成的所有事件组成(。从我在制作中看到的内容来看,快照每n-th提交都存储(例如每5个提交(。因此它们存储在写作方面。这是因为快照仅在特定的汇总版本的上下文中具有含义。
将您的业务逻辑放在实体或价值上。在那里为域服务而努力。