在尝试对Commanded中的传奇模式做出反应之前先应用状态

时间:2018-06-20 14:53:15

标签: elixir domain-driven-design cqrs event-sourcing commanded

在事件源/ CQRS框架命令中使用Commanded.ProcessManagers.ProcessManager模块实现传奇模式时,我遇到了问题。

在发票环境中,我需要为发票实现批量创建机制。大量创造既可以作为一个整体又可以作为一个传奇。聚合允许开始和完成批量创建。传奇通过发出命令来创建发票并将其ID保持为传奇状态,从而对“批量创建开始”事件做出反应。然后,传奇通过侦听命令其存在的发票实例的成功或失败事件来跟踪发票创建的状态。一旦每个发票实例都报告成功或失败,则传奇应发出命令以停止批量创建。

为此,跟踪每个发票实例的当前状态为in progresscreatedfailed会很有帮助。我尝试在apply回调中实现此功能,原则上来说,效果很好。

现在的问题是,apply回调总是在handle回调之后被调用。因此,应该在传奇故事做出反应之后更新传奇故事状态。这似乎是违反直觉的,因此,handle回调中可用的状态不能用于正确地做出反应。

我认为,传奇模式在很多方面都是聚合模式的倒置。虽然首先将命令处理为一个域事件,然后在聚合的情况下将此域事件应用到状态是有用的,但我认为,对于一个传奇事件,该域事件是已经发生的事情的文档,应先对状态应用,然后再对此做出反应。

现在我的问题是:是否可以将apply模块的Commanded配置为首先handle,然后是Commanded.ProcessManagers.ProcessManager?还是这从根本上来说是一个错误,需要大体上修复?

1 个答案:

答案 0 :(得分:1)

apply/2之后调用handle/2回调是设计使然,无法将Commanded配置为不同的行为。

我同意您的推理,在尝试处理事件以产生任何命令之前,将事件应用于流程管理器的状态更有意义。这似乎是对Commanded进行的值得更改,可以通过您已经提出的问题(#176)进行跟踪。

同时,您可以按以下方式实施流程管理器(传奇):

defmodule InvoicingProcessManager do
  use Commanded.ProcessManagers.ProcessManager,
    name: __MODULE__,
    router: InvoicingRouter

  defstruct [
    :batch_uuid,
    pending_invoice_ids: MapSet.new()
  ]

  def interested?(%InvoiceBatchStarted{batch_uuid: batch_uuid}), do: {:start, batch_uuid}
  def interested?(%InvoiceCreated{batch_uuid: batch_uuid}), do: {:continue, batch_uuid}
  def interested?(%InvoiceFailed{batch_uuid: batch_uuid}), do: {:continue, batch_uuid}
  def interested?(%InvoiceBatchStopped{batch_uuid: batch_uuid}), do: {:stop, batch_uuid}
  def interested?(_event), do: false

  # Event handlers

  def handle(%InvoicingSaga{}, %InvoiceBatchStarted{} = started) do
    %InvoiceBatchStarted{batch_uuid: batch_uuid, invoice_ids: invoice_ids} = started

    Enum.map(invoice_ids, fn invoice_id ->
      %CreateInvoice{
        invoice_id: invoice_id,
        batch_uuid: batch_uuid
      }
    end)
  end

  def handle(%InvoicingSaga{}, %InvoiceCreated{invoice_id: invoice_id}),
    do: attempt_stop_batch(pm, invoice_id)

  def handle(%InvoicingSaga{}, %InvoiceFailed{invoice_id: invoice_id}),
    do: attempt_stop_batch(pm, invoice_id)

  ## State mutators

  def apply(%InvoicingSaga{} = pm, %InvoiceBatchStarted{} = started) do
    %InvoiceBatchStarted{batch_uuid: batch_uuid, invoice_ids: invoice_ids} = started

    %InvoicingSaga{
      transfer
      | batch_uuid: batch_uuid,
        pending_invoice_ids: MapSet.new(invoice_ids)
    }
  end

  def apply(%InvoicingSaga{} = pm, %InvoiceCreated{invoice_id: invoice_id}) do
    %InvoicingSaga{pm | pending_invoice_ids: invoice_completed(pm, invoice_id)}
  end

  def apply(%InvoicingSaga{} = pm, %InvoiceFailed{invoice_id: invoice_id}) do
    %InvoicingSaga{pm | pending_invoice_ids: invoice_completed(pm, invoice_id)}
  end

  ## Private helpers

  def attempt_stop_batch(%InvoicingSaga{batch_uuid: batch_uuid} = pm, invoice_id) do
    pending_invoices = invoice_completed(pm, invoice_id)

    case empty?(pending_invoices) do
      true -> %StopInvoiceBatch{batch_uuid: batch_uuid}
      false -> []
    end
  end

  defp invoice_completed(%InvoicingSaga{pending_invoice_ids: pending_invoice_ids}, invoice_id) do
    MapSet.delete(pending_invoice_ids, invoice_id)
  end

  defp empty?(map_set, empty \\ MapSet.new())
  defp empty?(%MapSet{} = empty, %MapSet{} = empty), do: true
  defp empty?(%MapSet{}, %MapSet{}), do: false
end