什么是事件溯源的适当事件级别?

What is a proper level of events for event sourcing?

有时我无法决定一个事件应该代表什么。例如,这是银行帐户的简化 Ledger

Ledger {
    date : Date;
    amount : Int;
    cleared : String;
}

用户通过输入参考文本来清除分类帐。或者用户可以通过将文本设置为空字符串来删除清除。

我的问题是,在使用事件溯源跟踪更改时,我是否应该为用户打算执行的操作创建事件,例如:

Event clearLedger(clearText : String)
Event removeClearing()

或者我应该为幕后发生的事情制作一个更通用的事件,这在两种情况下都适用:

Event updateLedger(clearText : String)

这可以一直带到非常基本的类似 CRUD 的级别,最终在数据库的事务日志级别结束,所以这里有任何指导方针吗?

课程用马。

My question is, when tracking changes with event sourcing, should I create events for what the user intends to do

可能吧。在实体边界内,这无关紧要;但是从外部观察历史,能够从事件中识别出变化的背景是非常有用的。想想pub/sub;一旦您有了实体写入事件,您可能想要开始订阅这些事件。

例如,考虑更改客户资料中的地址。这可能是更正早期数据输入中的拼写错误(业务跟踪错误率以寻找可纠正的系统问题以改善客户体验),或者客户搬迁(在这种情况下,我们想向他们的新客户发送欢迎套件地址;或启动审核以根据现有政策审查他们的新地址)。

如果一切都在单个 AddressChanged 事件的保护下,您将失去这种灵活性。最好的情况是,您发现自己试图仅从数据中猜测变化的背景。

另一方面,如果这种灵活性没有带来任何价值,那么您将不需要它。

就是说,事件溯源 CRUD 很奇怪——如果您不重视作为第一批 class 公民的更改,那么为什么要对实体进行事件溯源?写出聚合状态就简单多了。

What is the difference between writing out the events and writing out the aggregate state?

不多;阅读它们有点不同。

不那么神秘:写出聚合状态类似于写出聚合现在的样子。通常,这是由持久性组件完成的,它将当前状态序列化为 DTO。例如,我们可以用 JSON 文档

表示 Ledger 的当前状态
{ "date"    : "2016-07-06"
, "amount"  : 40
, "cleared" : null
}

稍后要重新创建分类帐,我们只需从永久存储中获取 json 文档,然后让对象映射器开始工作。

写出历史,也就是事件,看起来会更像

[ { "event_type" : "LedgerCreated"
  , "data" 
  : { "date" : "2016-07-06"
    , "amount" : 40
    }
  }
, { "event_type" : "LedgerCleared"
  , "data"
  : { "reason" : "Because I said so" 
    }
  }
, { "event_type" : "ClearingRemoved"
  , "data"
  : {}
  }
]

稍后要重新创建分类帐,我们需要从永久存储中获取 json 文档,然后使用对象映射器创建有序的事件序列,在其初始 "seed" 状态,然后 将这些事件重新应用 到我们新的 Ledger 实体,通过重放其历史记录中的每个更改,有效地发现 Ledger 现在的样子。

无论业务专家使用什么语言,我都愿意。如果他们使用 UpdateLedger,那么使用它。如果他们使用 LedgerCleared 使用那个。

事件代表一些业务操作,它们本身具有值得查看和分析的价值。例如,您可能想查看删除了多少次分类帐清算。如果这是您想要的并且这就是您的领域专家正在谈论的内容 - 那就去做吧。如果你想做 CRUD,就像@VoiceOnUnreason 写的那样,不要事件源 CRUD。从本质上讲,您可能不仅仅是在谈论领域事件。例如,你的聚合方法 clearLedgerremoveClearance 会产生什么样的领域事件。如果两个不同的业务操作产生相同的事件时间,那将很奇怪。

你的候选人 UpdateLedger 会产生一个通用的 LedgerUpdated。然后几周后,您将另一个操作添加到 UpdateLedger 中,它将变成一个庞大的方法,其中包含许多正在检查 nulls/empties 的参数和许多 if 语句。这可能是每个基本 DDD talk/book/presentation 开始的第一个例子,作为糟糕设计的例子,首先做 DDD 的原因...

关于这个东西有两个简单的规则:

  • 如果您在命令、方法和事件中使用 CreateUpdateDelete - 这有 CRUD 的味道。问问自己,你所做的是否是 DDD
  • 在命令处理程序(也称为应用程序服务)中包含 if 语句,它检查参数并决定如何更改聚合状态或调用哪个聚合方法,这是一种粒度不够、横切关注点和违反单一职责原则