为什么命令和事件要分别表示?

104

在强调事件的架构中,命令和事件有什么区别?我唯一能看出来的区别是,命令通常由系统外的执行者发起/调用,而事件似乎是由系统内的处理程序和其他代码发起的。然而,在许多示例应用程序中,它们具有不同(但功能上类似)的接口。


当你使用“命令”和“事件”这些词语时,它们的确切含义非常重要。 - Doc Brown
以典型的CQRS/DDD项目为例。 - alphadogg
5
我的理解是:命令必须由一个接收者处理(该接收者可以拒绝命令),而事件可以由0到n个接收者处理。 - Levi Fuller
11个回答

193

指令可能会被拒绝。

事件已经发生。

这可能是最重要的原因。在事件驱动体系结构中,毫无疑问引发的事件代表了已经发生的事情。

现在,因为指令是我们想要发生的事情,而事件是已经发生的事情,所以我们在命名这些东西时应该使用不同的动词。这驱动着不同的表示。

我可以看到的是,指令通常是由系统外的执行者发起/调用的, 而事件似乎是由处理程序和其他代码在系统内发起的

这是它们分别被表示的另一个原因。概念上的清晰度。

指令和事件都是消息。但它们实际上是不同的概念,并且应该明确地建模。


好的,它们之间有实际的、实现层面的区别吗?例如,不同的接口? - alphadogg
14
是的,我会说主要是在调度方面。命令被调度到单个处理程序,但事件被调度到多个侦听器。尽管实现差异在总线中,但我仍然使用分离的事件和命令接口,以便每个总线只处理它可以处理的消息。 - quentin-starin
15
使用命令时使用“发送”一词(关注此操作的目标),而使用事件时使用“发布”(不关心另一端是谁)。 - Yves Reynhout
5
我知道这已经过去了一段时间,但我真希望我能将你的回答点赞10次……我本来想要探讨"为什么不把所有事情都视为事件,有些只是被执行而已",比如"请求某事"然后"某事发生了"。 - Michael Wasser
1
考虑一个实体被持久化在MySQL中并触发了实体索引事件。同样的事件被索引服务所监听,当接收到该事件时,索引服务会获取实体并将其索引在Elasticsearch中。从索引服务的角度来看,你仍然会称其为事件还是命令? - Technoshaft
显示剩余3条评论

18

事件是过去发生的事实。

命令只是一个请求,因此可能会被拒绝。

命令 事件
目的 调用行为 某事发生了
所有权 命令由消费者拥有 事件由发布者拥有
消费者 一个消费者 零个或多个消费者
发送方 许多发送方 单个发布者
命名 动词 过去时

命令的一个重要特性是它应该由单个接收方处理一次。这是因为命令是您想要在应用程序中执行的单个操作或事务。例如,同一订单创建命令不应被处理多次。这是命令和事件之间的重要区别。事件可以被多次处理,因为许多系统或微服务可能对该事件感兴趣。'msdn'


9
除了这里所有的答案之外,事件处理程序还可以在接收到事件通知后触发命令。例如,在创建客户后,您还想初始化一些账户值等内容。在将事件添加到事件分派器中并由CustomerCreatedEventHandler对象接收时,此处理程序可以触发一个命令的发送,该命令将执行您需要的任何操作。
另外,还有领域事件和应用程序事件。它们的区别只是概念上的。您希望首先调度所有的领域事件(其中一些可能会产生应用程序事件)。我的意思是什么?
在客户创建事件发生后初始化帐户是一个领域事件。向客户发送电子邮件通知是应用程序事件。
你不应该混淆它们的原因很明确。如果您的SMTP服务器暂时关闭,那并不意味着您的领域操作应受此影响。您仍然希望保持聚合的无损状态。
我通常在聚合根级别将事件添加到我的分派程序中,这些事件既可以是领域事件也可以是应用程序事件。可以是两者,也可以是多个。一旦我的命令处理程序完成,我回到执行命令处理程序的代码堆栈,然后检查我的分派器并分派任何其他领域事件。如果所有这些都成功,则关闭事务。
如果有任何应用程序事件,则此时发送它们。发送电子邮件不一定需要打开到数据库的连接或打开交易范围。
我偏离了原始问题,但您也需要了解如何在概念上处理事件的不同方式。
然后您还有Saga......但这远远超出了此问题的范围 :)
有道理吗?

但是你同样可以认为你有域事件,应用程序事件和用户事件。然后这只是关于事件来源的问题。我确实明白这种区别可能会有用,但也许更多地是在区分请求和由其产生的响应/操作方面。但即使在那里,我还没有完全被说服。需要做更多的调查。 - Arwin
所以,既然我读了自己的回答,我可以看到混淆。事件实际上可能是一个。事件。然后您有可能具有该区别的处理程序。您可能会为同一事件拥有两个处理程序,然后可以在不同的上下文中触发它们。您可以决定在活动事务中触发“域事件处理程序”,然后在此之后,可以触发您的“应用程序事件处理程序”。我只是不明白在交易中发送电子邮件或短信的意义所在。也许您可以将“任务”添加到数据库中,并由另一个进程执行它们,例如发送电子邮件。这有意义吗? - Pepito Fernandez

8

在研究了一些例子,特别是Greg Young的演讲后(http://www.youtube.com/watch?v=JHGkaShoyNs),我得出结论:命令是多余的。它们只是来自用户的事件,表示他们按下了那个按钮。你应该以与其他事件完全相同的方式存储这些数据,因为它是数据,而你不知道将来是否需要使用它。你的用户添加了该商品,然后又从购物篮中删除了该商品,或者至少尝试过。您以后可能希望使用此信息在稍后的日期提醒用户。


按照@quentin-starin所描述的方式,将事件视为已发生的事情,将命令视为我们想要发生的事情(一种请求),并不会阻止记录按钮按下事件,只是这些事件不一定会导致命令的执行,或者导致已被执行的命令。 - fractor
我仍然认为命令是多余的。我只是称呼我所做的功能事件溯源。 我的最近一篇博客,介绍了ES和F# Elm作为一个完整系统: http://anthonylloyd.github.io/blog/2016/11/27/event-sourcing - Ant
3
命令将本地事件与远程操作解耦。在您的示例中,除非使用者也知道UserSelectedMenu或ScriptDidSomething事件,否则对UserPressedButton事件的使用者不会做出反应。此外,命令通常针对特定的使用者;同样,在您的示例中,UserPressedButton事件的使用者无法判断用户是否选中了“确认”复选框,除非我们增加更多的耦合。使用命令可以根据发送者的状态甚至外部策略来执行操作。单独使用事件几乎不可能实现这一点。 - Doctor Eval
看看这个项目 - https://github.com/gregoryyoung/m-r 。它同时使用命令和事件。 - xhafan
8
在考虑最严格的实现——事件溯源时,更容易区分命令和事件。在这里,事件是唯一的真相来源。只需回放事件即可随时构建完整状态。相比之下,命令是请求,_可能_会导致事件,但也可能被拒绝。对于重建状态,命令并不重要。因此,如果系统无法处理命令(例如由于验证错误),那么没关系。如果系统无法处理事件,您的状态将被破坏。 - sven

4

补充以下这些精彩的答案。我想指出与耦合相关的差异。

命令是针对特定处理器的。因此,命令发起者和处理器之间存在某种程度的依赖/耦合。

例如,UserService在创建新用户时向EmailService发送“发送电子邮件”命令。

UserService知道它需要EmailService,这已经是耦合了。如果EmailService更改其API模式或出现故障,它会直接影响UserService功能。


事件不针对任何特定的事件处理程序。因此,事件发布者变得松散耦合。它不关心哪个服务消费其事件。甚至可以有0个事件的使用者也是有效的。

例如,UserService在创建新用户时发布“User Created Event”。可能EmailService会消费该事件并向用户发送电子邮件。

这里UserService不知道EmailService。它们是完全解耦的。如果EmailService出现故障或更改业务规则,我们只需要编辑EmailService即可。


这两种方法都有优点。纯事件驱动的设计很难跟踪,因为它过于松散耦合,特别是对于大型系统而言。而命令密集的架构具有高度的耦合性。因此,达到良好平衡是理想的。

希望这有意义。


2

它们被单独表示是因为它们代表非常不同的事物。如@qstarin所说,命令是可以被拒绝的消息,成功后会产生一个事件。

命令和事件都是Dto(数据传输对象),它们是消息,当创建实体时,它们倾向于看起来非常相似,但从那时起,未必如此。

如果您担心重用,那么您可以使用命令和事件作为(message)负载的信封

class CreateSomethingCommand
{
    public int CommandId {get; set;}

    public SomethingEnvelope {get; set;}
 }

然而,我想知道你为什么在问这个问题:D,也就是说你是否有太多的命令/事件?

1
不,我没有太多的经验,因为我正在尝试构建我的第一个这样的系统! :) 我处于学习模式。我试图理解CommandHandlers和EventHandlers是否有任何不同,或者基本上具有相同的接口。 - alphadogg
一个有趣的学习点是,命令和事件可以是不同的。例如,假设你有一个CheckoutCartCommand,那么事件可能会比命令包含更多的数据,而且可能会有很多命令。强烈建议您查看https://github.com/MarkNijhof/Fohjin和https://github.com/gregoryyoung/m-r。 - roundcrisis
关于你的例子,信封通常在外面,而不是里面(例如肥皂信封)。而且我认为缺少一个属性名(Payload?)。 - Yves Reynhout
@Yves:鉴于Something信封中包含了命令所必需的信息(如果这是客户创建,想想电子邮件),我认为这样做相当奇怪,你不觉得吗? - roundcrisis
我认为你没有理解信封的概念。如果你有特定于命令的内容,那么请将它们放在命令中,可以作为有效载荷或标头(带外数据)。但是不要将有效载荷/标头称为信封。 - Yves Reynhout

2

基于命令重新计算状态通常是不可行的,因为它们可能每次被处理时都会产生不同的结果。

例如,想象一个GenerateRandomNumber命令。每次调用它都会产生一个不同的随机数X。因此,如果您的状态依赖于这个数字,每次从命令历史记录重新计算状态时,您将得到不同的状态。

事件解决了这个问题。当您执行一个命令时,它会产生一系列事件,表示命令执行的结果。例如,GenerateRandomNumber命令可以生成一个GeneratedNumber(X)事件,记录生成的随机数。现在,如果您从事件日志中重新计算状态,您将始终获得相同的状态,因为您始终使用特定命令执行生成的相同数字。

换句话说,命令是带有副作用的函数,事件记录了特定命令执行的结果。

注意: 您仍然可以记录命令历史以进行审计或调试。关键是要重新计算状态,您需要使用事件历史记录,而不是命令历史记录。


一个很好的观察。请原谅我的回答涉及相同的问题,同时添加了更多的细微差别。 - Mario

2
除了上述概念上的不同之外,我认为还有一个与常见实现相关的差异:
事件通常在后台循环中处理,需要轮询事件队列。任何对事件感兴趣的人通常都可以注册一个回调函数,在事件队列处理时调用该函数。因此,一个事件可能涉及多个对象。
命令可能不需要以这种方式进行处理。命令的发起者通常可以访问命令的执行者。例如,这可以是发送给执行者的消息队列。因此,命令是针对单个实体设计的。

1
我认为需要补充quentin-santin的答案是他们:
将请求封装为对象,从而可以让你使用不同的请求参数化客户端、排队或记录请求,并支持可撤销操作。 来源

1

让我们将函数核心,命令式外壳模式加入其中。事件对应于函数核心,而命令对应于命令式外壳。也就是说,事件驻留在纯净的环境中,而命令则驻留在不纯净的环境中。像任何CLI一样,外壳处理不纯净的部分。

这可以从命令被验证并有时被拒绝来看出。还可以从命令依赖于在事件世界中不允许的操作中看出。例如,在Backgammon游戏中掷骰子涉及发出roll命令,其生成的结果将被记录在rolled事件中。这种分离的价值可以进一步体现在源或重放事件的能力上。

虽然实时事件可能会冒泡到外壳并导致发出新的命令,但重放事件则不能。它们必须不会引起进一步的副作用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接