有时我很难决定事件应该代表什么。例如,以下是银行账户的简化Ledger
:
Ledger {
date : Date;
amount : Int;
cleared : String;
}
用户通过输入参考文本来清除分类帐。或者,用户可以通过将文本设置为空字符串来删除清除。
我的问题是,当使用事件源跟踪更改时,我是否应该为用户想要做的事情创建事件,比如:
Event clearLedger(clearText : String)
Event removeClearing()
或者我应该为幕后发生的事情制作一个更通用的事件,在这两种情况下都有效:
Event updateLedger(clearText : String)
这可能一直被带到一个非常基本的类似CRUD的级别,最终到达数据库的事务日志级别,那么这里有什么指导方针吗?
课程用马。
我的问题是,当使用事件源跟踪更改时,我是否应该为用户打算做的创建事件
可能。在实体边界内,这并不重要;但是,从外部审视这段历史,能够从事件中识别出变化的背景是非常有用的。想想酒吧;一旦您有了一个编写事件的实体,您很可能想要开始订阅这些事件。
例如,考虑更改客户档案中的地址。这可能是对早期数据输入中的拼写错误的纠正(业务部门跟踪错误率,寻找可纠正的系统性问题,以改善客户体验),也可能是客户的搬迁(在这种情况下,我们希望向他们的新地址发送欢迎包;或启动审计,根据他们的现有政策审查他们的新住址)。
如果所有内容都在一个AddressChanged事件的保护伞下,那么您就失去了这种灵活性。在最好的情况下,您会发现自己试图仅从数据中猜测更改的上下文。
另一方面,如果这种灵活性没有带来任何价值,那么你就不需要它了
也就是说,事件源CRUD很奇怪——如果你不把这些变化视为头等公民,那么为什么要为实体提供事件源呢?写出聚合状态要简单得多。
写出事件和写出聚合状态有什么区别?
不多;读起来有点不一样。
不那么神秘:写出聚合状态类似于写出聚合现在的样子。通常,这是由持久性组件完成的,该组件将当前状态序列化为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文档,然后使用对象映射器创建一个有序的事件序列,在其初始"种子"状态下创建一个分类账,然后将这些事件重新应用到我们的新分类账实体,通过回放其历史中的每一个更改,有效地发现分类账现在的样子。
无论业务专家使用什么语言,我都愿意为您服务。如果他们使用UpdateLedger,那么就使用它。如果他们使用LedgerCleared,请使用它。
事件代表一些业务操作,它们本身就有价值,可以查看和分析。例如,您可能想查看总账清除的次数。如果这是你想要的,这也是你的领域专家正在谈论的——那就去做吧。如果你想做CRUD,就像@VoiceOnUnreason所写的那样,不要使用事件源CRUD。从本质上讲,您可能不仅仅是在谈论域事件。例如,聚合方法clearLedger
和removeClearance
将生成什么类型的域事件。如果两个不同的业务运营产生相同的事件时间,那将是奇怪的。
您的候选UpdateLedger
将生成一个通用LedgerUpdated
。几周后,您将另一个操作添加到UpdateLedger
中,它将成为一个庞大而臃肿的方法,其中包含许多正在检查null/empty的参数和许多if语句。这可能是每个基本的DDD演讲/书籍/演示的第一个例子,作为糟糕设计的例子,首先做DDD的原因。。。
关于这种东西有两条简单的规则:
- 如果在命令、方法和事件中使用
Create
、Update
和Delete
,则会闻到CRUD的味道。问问自己你所做的是否是DDD - 在命令处理程序(又称应用程序服务)中使用
if
语句会检查参数并决定如何更改聚合状态或调用哪个聚合方法,这是一种粒度不足、交叉问题和违反单一责任原则的味道