2015-12-16 7 views
2

Многие статьи о CQRS подразумевают, что саги имеют внутреннее состояние и должны быть сохранены в хранилище событий. Я не понимаю, почему это необходимо.Почему саги (ака, менеджеры процессов) содержат внутреннее состояние и почему они сохраняются в хранилище событий?

Например, у меня есть три агрегата: Order, Invoice и Shipment. Когда заказчик отправляет заказ, начинается процесс заказа. Тем не менее, отправка не может быть отправлена ​​до момента оплаты счета-фактуры, и отгрузка сначала была подготовлена.

  1. Клиент делает заказ с помощью команды PlaceOrder.
  2. OrderCommandHandler звонки OrderRepository::placeOrder().
  3. Метод OrderRepository::placeOrder() возвращает событие OrderPlaced, которое хранится в EventStore и отправляется по адресу EventBus.
  4. Событие OrderPlaced содержит orderId и предопределяет invoiceId и shipmentId.
  5. OrderProcess («сага») получает событие OrderPlaced, создавая счет и при необходимости готовя к отправке (достижение идемпотенции в обработчике событий). 6a. В какой-то момент времени OrderProcess принимает событие InvoicePaid. Он проверяет, была ли отгрузка подготовлена ​​путем поиска груза в ShipmentRepository, и если да, отправляет груз. 6b. В какой-то момент времени OrderProcess принимает событие ShipmentPrepared. Он chekcs, чтобы узнать, был ли оплачен счет, просмотрев счет-фактуру в 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

Почти одинаковый вопрос был задан вчера в почтовом списке DDD/CQRS https://groups.google.com/forum/#!topic/dddcqrs/danMYZS6kKg –

+0

Это был я. В CQRS/ES «лучшая практика» существует много разных мнений. Чем больше сеть, тем больше рыбы. – magnus

ответ

1

Что вы имеете в виду, кажется, вдоль линий оркестровки (остроумие менеджер процесса) против хореографии.

Хореография работает абсолютно нормально, но у вас не будет диспетчера процессов в качестве первоклассного гражданина. Каждый обработчик команд определит, что делать. Даже мой текущий проект (декабрь 2015) довольно часто использует хореографию с помощью брокера интеграции webMethods. Сообщения могут даже нести какое-то состояние вместе с ними. Однако, когда что-то должно происходить параллельно, вы довольно валяетесь.

Соответствующий service orchestration vs choreography question демонстрирует эти концепции довольно красиво. Один из ответов содержит красивое графическое представление и, как указано в ответе, более сложные взаимодействия обычно требуют состояния для процесса.

Я считаю, что вам обычно требуется состояние при взаимодействии с сервисами и конечной точкой, находящимися вне вашего контроля. Человеческое взаимодействие, такое как авторизация, также требует такого типа состояния.

Если вы можете уйти с отсутствием состояния для диспетчера процессов, возможно, все в порядке. Однако позже вы можете столкнуться с проблемами. Например, некоторые низкоуровневые/основные/инфраструктурные службы могут охватывать различные процессы. Это может вызвать проблемы в сценарии хореографии.

1

Команды могут не работать.

Это основная проблема; вся причина, по которой мы имеем агрегаты в первую очередь, заключается в том, чтобы они могли защитить бизнес от недействительных изменений состояния. Итак, что происходит в onOrderPlaced(), если команда createInvoice выходит из строя?

Кроме того (хотя и несколько связанный) вы потеряны во времени. Менеджеры процессов обрабатывают события; события - это вещи, которые уже произошли в прошлом. Ergo - менеджеры процессов работают в прошлом. В очень реальном смысле они не могут даже разговаривать с кем-либо, кто видел более недавнее событие, чем тот, который они обрабатывают прямо сейчас (на самом деле, они могут быть первым обработчиком, чтобы увидеть это событие, что означает, что все остальные являются шаг в прошлом).

Вот почему вы не можете запускать команды синхронно; ваш обработчик событий в прошлом, и агрегат не может защитить свой инвариант, если он не работает в настоящем. Вам нужна асинхронная отправка, чтобы запустить команду против правильной версии агрегата.

Следующая проблема: при отправке команды асинхронно вы не можете напрямую наблюдать результат. Это может привести к ошибке или заблудиться в пути, и обработчик события не узнает. Единственный способ определить, что команда преуспела, - это наблюдать генерируемое событие.

Следствием является то, что диспетчер процессов не может отличить команду, которая не удалась из успешной команды (но событие еще не стало видимым). Чтобы поддерживать конечную sla, вам нужна служба синхронизации, которая время от времени просыпает диспетчера процессов, чтобы проверять вещи.

Когда диспетчер процессов просыпается, ему необходимо знать, завершена ли работа.

С государством все намного проще управлять. Менеджер процессов может повторно выполнить, возможно, потерянные команды, чтобы убедиться, что они прошли, не наводя домен с помощью команд, которые уже преуспели. Вы можете моделировать часы, не бросая события синхронизации в сам домен.