很多关于CQRS的文章都暗示传奇有内部状态,必须保存到活动商店。我不明白为什么这是必要的。
例如,假设我有三个聚合:Order
,Invoice
和Shipment
。当客户下订单时,订单处理开始。但是,在发票已经支付并且货物已经准备好之前,不能发货。
PlaceOrder
命令下订单。OrderCommandHandler
来电OrderRepository::placeOrder()
。OrderRepository::placeOrder()
方法会返回OrderPlaced
个事件,该事件存储在EventStore
中并沿EventBus
发送。OrderPlaced
事件包含orderId
并预先分配invoiceId
和shipmentId
。OrderProcess
(“saga”)收到OrderPlaced
事件,创建发票并在必要时准备货件(在事件处理程序中实现幂等)。
6A。在某个时间点,OrderProcess
会收到InvoicePaid
事件。它通过在ShipmentRepository
中查找货件来检查货物是否已准备好,如果是,则发送货件。
6B。在某个时间点,OrderProcess
会收到ShipmentPrepared
事件。通过在InvoiceRepository
中查找发票来查看是否已支付发票,如果是,则发送货件。对于所有经验丰富的DDD / CQRS / ES专家,你能告诉我我错过了什么概念以及为什么这种“无国籍传奇”的设计不起作用?
class OrderCommandHandler {
public function handle(PlaceOrder $command) {
$event = $this->orderRepository->placeOrder($command->orderId, $command->customerId, ...);
$this->eventStore->store($event);
$this->eventBus->emit($event);
}
}
class OrderRepository {
public function placeOrder($orderId, $customerId, ...) {
$invoiceId = randomString();
$shipmentId = randomString();
return new OrderPlaced($orderId, $customerId, $invoiceId, $shipmentId);
}
}
class InvoiceRepository {
public function createInvoice($invoiceId, $customerId, ...) {
// Etc.
return new InvoiceCreated($invoiceId, $customerId, ...);
}
}
class ShipmentRepository {
public function prepareShipment($shipmentId, $customerId, ...) {
// Etc.
return new ShipmentPrepared($shipmentId, $customerId, ...);
}
}
class OrderProcess {
public function onOrderPlaced(OrderPlaced $event) {
if (!$this->invoiceRepository->hasInvoice($event->invoiceId)) {
$invoiceEvent = $this->invoiceRepository->createInvoice($event->invoiceId, $event->customerId, $event->invoiceId, ...);
$this->eventStore->store($invoiceEvent);
$this->eventBus->emit($invoiceEvent);
}
if (!$this->shipmentRepository->hasShipment($event->shipmentId)) {
$shipmentEvent = $this->shipmentRepository->prepareShipment($event->shipmentId, $event->customerId, ...);
$this->eventStore->store($shipmentEvent);
$this->eventBus->emit($shipmentEvent);
}
}
public function onInvoicePaid(InvoicePaid $event) {
$order = $this->orderRepository->getOrders($event->orderId);
$shipment = $this->shipmentRepository->getShipment($order->shipmentId);
if ($shipment && $shipment->isPrepared()) {
$this->sendShipment($shipment);
}
}
public function onShipmentPrepared(ShipmentPrepared $event) {
$order = $this->orderRepository->getOrders($event->orderId);
$invoice = $this->invoiceRepository->getInvoice($order->invoiceId);
if ($invoice && $invoice->isPaid()) {
$this->sendShipment($this->shipmentRepository->getShipment($order->shipmentId));
}
}
private function sendShipment(Shipment $shipment) {
$shipmentEvent = $shipment->send();
$this->eventStore->store($shipmentEvent);
$this->eventBus->emit($shipmentEvent);
}
}
答案 0 :(得分:4)
命令可能会失败。
这是主要问题;我们首先聚合的全部原因是,它们可以保护业务免受无效状态变化的影响。那么如果createInvoice命令失败,onOrderPlaced()会发生什么?
此外(尽管有些相关)你会迷失方向。流程经理处理事件;事件是过去已经发生的事情。 Ergo - 流程经理过去一直在运行。从一个非常真实的意义上说,他们甚至不能与任何看过最新事件的人交谈,而不是他们现在正在处理的事件(事实上,他们可能是第一个看到这个事件的人,这意味着其他人是过去的一步。)
这就是为什么你不能同步运行命令;你的事件处理程序是过去的,除非它在当前运行,否则聚合不能保护其不变量。您需要异步分派才能使命令针对聚合的正确版本运行。
下一个问题:当您异步调度命令时,您无法直接观察结果。它可能会失败,或者在途中迷路,并且事件处理程序不会知道。它可以确定命令成功的唯一方法是观察生成的事件。
结果是进程管理器无法区分失败的命令和成功的命令(但事件尚未可见)。为了支持有限的sla,你需要一个定时服务来不时地唤醒流程管理器来检查事物。
当流程经理醒来时,需要状态才能知道它是否已完成工作。
有了国家,一切都变得如此简单易于管理。进程管理器可以重新发出可能丢失的命令,以确保它们通过,而不会使用已经成功的命令充斥域。您可以在不将时钟事件抛入域本身的情况下对时钟进行建模。
答案 1 :(得分:2)
你所指的似乎与编排(与流程管理者)和编排相关。
编舞工作绝对正常,但你不会有一个流程经理作为一等公民。每个命令处理程序将确定要执行的操作。即使我当前的项目(2015年12月)也使用webMethods集成代理进行编排。消息甚至可以携带一些状态。但是,如果需要同时进行任何操作,那么您的选择就相当了。
相关的service orchestration vs choreography question很好地展示了这些概念。其中一个答案包含一个很好的图形表示,如答案中所述,更复杂的交互通常需要该过程的状态。
我发现在与您无法控制的服务和端点交互时,您通常会要求状态。人工交互(例如授权)也需要这种类型的状态。
如果您没有专门为流程经理提供州,那么可能没问题。但是,稍后您可能会遇到问题。例如,某些低级/核心/基础结构服务可能跨越各种进程。这可能会导致编排场景中的问题。