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


77

在强调事件的体系结构中,命令和事件之间有什么区别?我能看到的唯一区别是命令通常是由系统外部的参与者来发起/调用的,而事件似乎是由系统中的处理程序和其他代码来发起的。但是,在我看到的许多示例应用程序中,它们具有不同(但功能相似)的接口。


这在很大程度上取决于您使用“命令”和“事件”一词时要牢记什么。
Doc Brown

以您的典型CQRS / DDD项目为例。
alphadogg

4
我的理解:命令必须由一个接收器(可以拒绝命令)处理,事件可以由0 ... n个接收器处理。
Levi Fuller '18

Answers:


155

命令可以被拒绝。

事件发生了。

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

现在,由于命令是我们想要发生的事情,而事件是已经发生的事情,因此在命名这些事物时,我们应该使用不同的动词。这将驱动单独的表示。

我可以看到的是,命令通常是由系统外部的参与者来发起/调用的,而事件似乎是由系统中的处理程序和其他代码来发起的

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

命令和事件都是消息。但是它们实际上是独立的概念,应该对概念进行显式建模。


好的,它们之间在实用性,实现级别上是否有所不同?例如,不同的界面?
alphadogg 2011年

9
是的,我主要是在调度中说。命令被分派到单个处理程序,但事件被分派到多个侦听器。诚然,实现方面的差异在总线上,但是我仍然使用单独的事件和命令接口,因此每个总线仅接收其可以接收的消息。
2011年

8
使用命令时,使用“发送”一词(您关心此操作的目标),而使用事件时,使用“发布”(您不在乎谁在另一端)。
伊夫·雷恩豪特

4
@ quentin-starin我知道这很老了,但是我希望我可以像10次一样投票赞成你的答案...我打算走下一条路,“为什么不是所有的事情都发生了,有些只是采取了行动” -例如“ SomethingRequested”,然后是“ SomethingHappened”
Michael Wasser

1
考虑一个实体在mysql中持久存在并引发事件entityIndexed。索引服务侦听同一事件,该服务在检索事件时会获取实体并在elasticsearch中对其进行索引。从索引服务的角度来看,您还是称它为事件还是命令?
Technoshaft '18

7

另外,除了此处公开的所有答案之外,事件处理程序在接收到事件发生的通知后也可能能够触发命令。

举例来说,创建客户后,您还想初始化一些帐户值,等等。在客户AR将事件添加到EventDispatcher并由CustomerCreatedEventHandler对象接收到之后,此处理程序可以触发命令的调度,将执行您需要的任何内容,等等。

此外,还有DomainEvents和ApplicationEvents。区别仅仅是概念上的。您想首先分派所有域事件(其中一些事件可能会产生应用程序事件)。我是什么意思

在发生CustomerCreatedEvent之后初始化帐户是DOMAIN事件。向客户发送电子邮件通知是一个应用程序事件。

您不应该混合使用它们的原因很明显。如果您的SMTP服务器暂时关闭,这并不意味着您的DOMAIN OPERATION应该会受到影响。您仍然希望保持聚合的未损坏状态。

我通常在“聚合根”级别将事件添加到我的Dispatcher。此事件是DomainEvents或ApplicationEvents。可以是两者,也可以是很多。一旦我的命令处理程序完成,并且我回到堆栈中执行命令处理程序的代码,然后检查我的Dispatcher并分派任何其他DomainEvent。如果所有这些都成功,那么我将关闭交易。

如果我有任何应用程序事件,这是分发它们的时间。发送电子邮件不一定需要打开数据库连接,也不需要打开事务作用域。

我偏离了最初的问题,但对您来说,了解事件在概念上也可能会有所不同也很重要。

然后您有Sagas ....但是这个问题的范围有WAYYYY OFF :)

是否有意义?


但是,您可以轻松地说出您有域事件,应用程序事件和用户事件。然后就变成了事件的源头。我确实了解区分的用处,但在区分请求和由此产生的响应/动作方面也许更多。但是,即使在那儿,我还没有完全相信。需要做更多的调查。
阿温

因此,既然我阅读了自己的回复,就可以看到混乱。这些事件实际上可能是一个。活动。然后,您将拥有可能具有这种区别的处理程序。对于同一个事件,您可能有两个处理程序,然后可以在不同的上下文中触发它们。您可以决定在活动事务中触发“域事件处理程序”,然后再触发“应用程序事件处理程序”。我只是看不到在交易中发送电子邮件或短信的意义。也许您可以向数据库添加“任务”,然后让另一个进程执行它们,例如发送电子邮件。是否有意义?
Pepito Fernandez

6

事件是过去的事实。

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

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


5

在研究了一些示例,特别是Greg Young的演示文稿(http://www.youtube.com/watch?v=JHGkaShoyNs)之后,我得出的结论是命令是多余的。它们只是您用户的事件,他们确实按下了该按钮。您应该以与其他事件完全相同的方式存储它们,因为它们是数据,并且您不知道是否要在将来的视图中使用它。您的用户确实添加了商品,然后再将其从购物篮中删除,或者至少尝试这样做。您以后可能要使用此信息来提醒用户此事。


用@ quentin-starin描述的方式,将事件视为已发生的事情,将命令视为我们想要发生的事情(请求),不会停止记录按钮按下事件,只是这些事件没有发生。它不一定会导致命令,或者导致已执行命令。
fractor

我仍然认为命令是多余的。我只是称呼我从事功能性事件采购。我最近的一个博客,其中包含ES和F#Elm作为完整系统:anthonylloyd.github.io/blog/2016/11/27/event-sourcing
Ant的

2
命令使本地事件与远程操作脱钩。在您的示例中,除非用户也知道这些内容,否则UserPressedButton事件的使用者不会对UserSelectedMenu或ScriptDidSomething做出反应。另外,命令通常是针对特定的使用者的。再次,在您的示例中,除非我们添加了更多的耦合,否则UserPressedButton事件的使用者无法确定用户是否已选中“确认”复选框。使用命令,所采取的措施可能取决于发送者的状态,甚至取决于外部策略。单凭事件,几乎不可能做到这一点。
伊瓦尔医生

查看这个项目-github.com/gregoryyoung/mr。它同时使用命令和事件。
xhafan '18年

6
在考虑最严格的实现时,将事件与命令区分开会更加容易:事件源。这里的事件是真理的唯一来源。您可以随时通过仅重播事件来建立完整状态。相反,这些命令是可能导致事件但也可能被拒绝的请求。对于重建状态命令不重要。因此,如果您的系统无法处理命令(例如,由于验证错误),则可以。如果系统无法处理事件,则状态将被破坏。
sven

2

之所以分开代表,是因为它们代表了截然不同的事物。正如@qstarin所说,命令是可以被拒绝的消息,成功后将产生一个事件。命令和事件是Dto,它们是消息,在创建和实体时它们看起来很相似,但是从那时起,不一定。

如果您担心重用,则可以使用命令和事件作为(消息)有效负载的信封

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

    public SomethingEnvelope {get; set;}
 }

但是,我想知道的是您为什么问:D,即您是否有太多的命令/事件?


1
不,我没有太多,因为我希望构建自己的第一个这样的系统!:)我处于学习模式。我试图了解CommandHandlers和EventHandlers是否做任何不同的事情,或者基本上具有相同的接口。
alphadogg 2011年

需要学习的有趣一点是命令和事件可以不同,例如说您有一个CheckoutCartCommand,该事件可能比命令具有更多的数据,也可能有很多命令。强烈建议您看看github.com/MarkNijhof/Fohjingithub.com/gregoryyoung/mr
roundcrisis 2011年

就您的示例而言,信封通常位于外部,而不是内部(例如,肥皂信封)。而且我认为缺少属性名称(有效载荷?)。
伊夫·雷恩豪特

@Yves:鉴于Something信封具有该命令必不可少的信息(如果这是客户创建的想法电子邮件),我会发现这很奇怪,您认为吗?
roundcrisis 2011年

我认为您不了解信封的概念。如果您有特定于命令的内容,则将它们作为有效负载或标头(带外)放入命令中。但是不要将有效载荷/标题称为信封。
伊夫·雷恩豪特

2

除了上面提到的概念差异外,我认为还有一些与常见实现相关的差异:

通常在需要轮询事件队列的后台循环中处理事件。通常,对事件采取行动的任何一方都可以注册一个回调,该回调是事件队列处理的结果。因此,一个事件可能是一对多的

命令可能不需要以这种方式处理。命令的发起者通常将有权访问命令的预期执行者。例如,这可以是发送给执行者的消息队列的形式。因此,命令旨在用于单个实体


1

我认为可以添加到quentin-santin的答案中的是:

将请求封装为对象,从而使您可以将具有不同请求,排队或记录请求的客户端参数化,并支持可撤消的操作。

来源


0

您不能基于命令重新计算状态,因为一般而言,每次处理命令时它们都会产生不同的结果。

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

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

换句话说,命令是具有副作用的功能,事件记录命令的特定执行的结果。

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


0

只是为了添加这些出色的答案。我想指出耦合方面的差异。

命令被定向到特定的处理器。因此,与命令启动器和处理器之间存在某种程度的依赖/耦合。

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

UserService知道它需要的事实,EmailService已经在耦合。如果EmailService更改其API模式或出现故障,则将直接影响该UserService功能。


事件不针对任何特定的事件处理程序。因此,事件发布者变得松散耦合。它不在乎什么服务消耗了它的事件。拥有0个事件使用者甚至是有效的。

例如,UserService创建新用户时,a将发布“用户创建的事件”。可能EmailService会消耗该事件并向用户发送电子邮件。

在这里UserService不知道EmailService。它们是完全分离的。如果EmailService出现问题或更改了业务规则,我们只需要编辑EmailService


两种方法都有优点。纯粹的事件驱动架构设计很难跟踪,因为它耦合得太松散,尤其是在大型系统上。与Command重型架构的耦合程度很高。因此,理想的平衡是理想的。

希望有道理。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.