mag*_*nus 5 domain-driven-design cqrs event-sourcing
很多关于CQRS的文章都暗示传奇有内部状态,必须保存到活动商店.我不明白为什么这是必要的.
例如,假设我有三个聚集:Order,Invoice和Shipment.当客户下订单时,订单处理开始.但是,在发票已经支付并且货物已经准备好之前,不能发送货件.
PlaceOrder命令下订单.OrderCommandHandler电话OrderRepository::placeOrder().OrderRepository::placeOrder()方法返回一个OrderPlaced事件,该事件存储在EventStore并沿其发送EventBus.OrderPlaced事件包含orderId和预分配a invoiceId和shipmentId.OrderProcess("传奇")接收的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);
    }
}
命令可能会失败。
这是首要问题;我们首先使用聚合的全部原因是,它们可以保护业务免受无效状态更改的影响。那么如果 createInvoice 命令失败,onOrderPlaced() 会发生什么?
此外(虽然有些相关)你迷失在时间里。流程管理器处理事件;事件是过去已经发生的事情。Ergo - 流程管理器在过去运行。在非常真实的意义上,他们甚至无法与看到比他们正在处理的事件更近期的事件的任何人交谈(事实上,他们可能是第一个看到此事件的处理者,这意味着其他所有人都是一步过去)。
这就是你不能同步运行命令的原因;您的事件处理程序已经过去,并且聚合无法保护其不变性,除非它在当前运行。您需要异步调度来让命令针对正确版本的聚合运行。
下一个问题:异步调度命令时,无法直接观察结果。它可能会失败,或者在途中迷路,而事件处理程序不会知道。它可以确定命令成功的唯一方法是观察生成的事件。
结果是流程管理器无法区分失败的命令和成功的命令(但事件尚未变得可见)。为了支持有限的 sla,您需要一个定时服务来不时唤醒进程管理器以检查事物。
当进程管理器醒来时,它需要状态来知道它是否已经完成了工作。
有了状态,一切都变得更容易管理。进程管理器可以重新发出可能丢失的命令以确保它们通过,而不会用已经成功的命令淹没域。您可以对时钟进行建模,而无需将时钟事件放入域本身。
您所指的似乎是编排(使用流程管理器)与编排的关系。
编排工作绝对没问题,但你不会有一个作为一等公民的流程经理。每个命令处理程序将确定要做什么。甚至我当前的项目(2015 年 12 月)也大量使用了 webMethods 集成代理的编排。消息甚至可能携带一些状态。然而,当任何事情需要并行发生时,你就会受到很大的打击。
相关的服务编排与编排问题很好地演示了这些概念。其中一个答案包含一个很好的图形表示,并且如答案中所述,更复杂的交互通常需要过程状态。
我发现在与您无法控制的服务和端点交互时,您通常需要状态。人机交互(例如授权)也需要这种类型的状态。
如果您可以避免没有专门针对流程管理器的状态,那么可能没问题。但是,稍后您可能会遇到问题。例如,某些低级/核心/基础设施服务可能跨越各种进程。这可能会导致编排场景出现问题。