为啥要分别表示命令和事件?
Posted
技术标签:
【中文标题】为啥要分别表示命令和事件?【英文标题】:Why are commands and events separately represented?为什么要分别表示命令和事件? 【发布时间】:2011-06-25 04:00:58 【问题描述】:强调事件的架构中的命令和事件有什么区别?我能看到的唯一区别是命令通常是由系统外部的参与者获取/调用的,而事件似乎是由系统中的处理程序和其他代码获取的。但是,在我见过的许多示例应用程序中,它们具有不同(但功能相似)的接口。
【问题讨论】:
这在很大程度上取决于您在使用“命令”和“事件”这两个词时的确切想法。 以你典型的CQRS/DDD项目为例。 我的理解:命令只能由一个接收者处理(可以拒绝命令),而事件可以被0...n个接收者处理。 【参考方案1】:可以拒绝命令。
事件发生了。
这可能是最重要的原因。在事件驱动架构中,毫无疑问,引发的事件代表了已经发生的事情。
现在,因为命令是我们想要发生的事情,而事件是已经发生的事情,所以我们在命名这些事情时应该使用不同的动词。这驱动了单独的表示。
我可以看到命令通常是 由外部的参与者发起/调用 系统,而事件似乎是 由处理程序和其他代码来源 一个系统
这是它们分开表示的另一个原因。概念清晰。
命令和事件都是消息。但它们实际上是独立的概念,并且应该明确地对概念进行建模。
【讨论】:
好的,它们之间是否存在实际的实施级别差异?例如,不同的界面? 是的,我会说主要是在调度方面。命令被分派给单个处理程序,但事件被分派给多个侦听器。当然,实现的差异在于总线,但我仍然使用单独的事件和命令接口,以便每个总线只接收它可以接收的消息。 对于命令,您使用“发送”一词(您关心此操作的目标),而对于事件,您使用“发布”(您不在乎谁在另一端)。 @quentin-starin 我知道这已经过时了,但我希望我能给你的答案投票 10 次......他们中的一些人只是采取行动”——例如“SomethingRequested”,然后是“SomethingHappened” 考虑一个实体被持久化在 mysql 中并引发事件 entityIndexed。索引服务侦听相同的事件,该服务在接收事件时获取实体并在弹性搜索中对其进行索引。从索引服务的角度来看,您仍将其称为事件或命令吗?【参考方案2】:事件是过去的事实。
命令只是一个请求,因此可能会被拒绝。
命令的一个重要特征是它应该是 单个接收器只处理一次。这是因为一个命令是 您要在应用程序中执行的单个操作或事务。 例如,不应处理相同的订单创建命令 不止一次。这是命令和命令之间的重要区别 事件。事件可能会被多次处理,因为许多系统或 微服务可能对此事件感兴趣。 'msdn'
【讨论】:
【参考方案3】:在研究了一些示例,尤其是 Greg Young 的演示文稿 (http://www.youtube.com/watch?v=JHGkaShoyNs) 之后,我得出的结论是命令是多余的。它们只是来自您的用户的事件,他们确实按下了该按钮。您应该以与其他事件完全相同的方式存储它们,因为它是数据,您不知道是否要在将来的视图中使用它。您的用户确实添加了该项目,然后从篮子中删除了该项目,或者至少尝试这样做。您以后可能希望使用此信息来提醒用户这一点。
【讨论】:
以@quentin-starin 所描述的方式,将事件视为已经发生的事情,将命令视为我们想要发生的事情(请求),并不会停止记录按钮按下事件,只是这些事件不一定会导致命令,或导致已被执行的命令。 我仍然认为命令是多余的。我只是把我所做的事情称为功能性事件溯源。我最近的一篇博客,将 ES 和 F# Elm 作为一个完整的系统:anthonylloyd.github.io/blog/2016/11/27/event-sourcing 命令将本地事件与远程操作分离。在您的示例中,UserPressedButton 事件的使用者不会对 UserSelectedMenu 或 ScriptDidSomething 做出反应,除非它也知道这些事情。此外,命令通常针对特定的消费者;同样,在您的示例中,除非我们添加更多耦合,否则 UserPressedButton 事件的使用者无法判断用户是否选中了“确认”复选框。使用命令,所采取的行动可能取决于发送者的状态,甚至是外部策略。仅凭事件,这几乎是不可能的。 看看这个项目 - github.com/gregoryyoung/m-r 。它同时使用命令和事件。 在考虑最严格的实现时,更容易区分命令和事件:事件溯源。在这里,事件是唯一的真相来源。您可以随时通过仅重播事件来构建完整状态。相反的命令是可能导致事件的请求,但也可能被拒绝。对于重建状态命令是不重要的。因此,如果您的系统无法处理命令(例如由于验证错误),那也没关系。如果您的系统无法处理事件,您的状态将被破坏。【参考方案4】:此外,除了此处公开的所有答案外,事件处理程序还可以在收到事件发生的通知后触发命令。
比如说,在你创建了一个客户之后,你还想初始化一些账户值等。在你的客户 AR 将事件添加到 EventDispatcher 并且由 CustomerCreatedEventHandler 对象接收之后,这个处理程序可以触发一个调度一个可以执行任何你需要的命令,等等。
此外,还有 DomainEvents 和 ApplicationEvents。区别只是概念上的。您想首先调度所有域事件(其中一些可能会产生应用程序事件)。这是什么意思?
在发生 CustomerCreatedEvent 后初始化帐户是 DOMAIN 事件。向客户发送电子邮件通知是一个应用程序事件。
你不应该混合它们的原因很清楚。如果您的 SMTP 服务器暂时关闭,这并不意味着您的 DOMAIN OPERATION 应该受到影响。您仍然希望保持聚合的未损坏状态。
我通常将事件添加到聚合根级别的调度程序。此事件是 DomainEvents 或 ApplicationEvents。可以是两者,也可以是其中的许多。一旦我的命令处理程序完成并且我回到堆栈中执行命令处理程序的代码,然后我检查我的调度程序并调度任何其他域事件。如果这一切都成功,那么我关闭交易。
如果我有任何应用程序事件,现在是分派它们的时候了。发送电子邮件不一定需要打开与数据库的连接,也不一定需要打开事务范围。
我偏离了最初的问题,但了解事件在概念上的不同处理方式也很重要。
那么你有 Sagas.... 但这超出了这个问题的范围 :)
有意义吗?
【讨论】:
但是你可以很容易地争辩说你有领域事件、应用程序事件和用户事件。然后它只是关于事件的来源是什么。我确实理解这种区别是如何有用的,但也许更多地是在区分请求和由此产生的响应/动作时。但即使在那里,我还没有完全相信。需要做更多的调查。 所以,现在我阅读了自己的回复,我可以看到混乱。这些事件实际上可能是一个。事件。然后你有可能有这种区别的处理程序。对于同一个事件,您可能有两个处理程序,然后您可以在不同的上下文中触发它们。您可以决定在活动事务中触发“域事件处理程序”,然后,您可以触发您的“应用程序事件处理程序”。我只是看不到在事务中发送电子邮件或短信的意义。也许您可以将“任务”添加到数据库并让另一个进程执行它们,例如发送电子邮件。有意义吗?【参考方案5】:它们被分开表示,因为它们代表了非常不同的事物。 正如@qstarin 所说,命令是可以被拒绝的消息,成功时会产生一个事件。 命令和事件是 Dto,它们是消息,它们在创建和实体时往往看起来非常相似,但从那时起,就不一定了。
如果您担心重用,那么您可以使用命令和事件作为(消息)有效负载的信封
class CreateSomethingCommand
public int CommandId get; set;
public SomethingEnvelope get; set;
但是,我想知道你为什么要问:D ie 你有太多的命令/事件吗?
【讨论】:
不,我没有太多,因为我正在寻找构建我的第一个这样的系统! :) 我处于学习模式。我试图了解 CommandHandlers 和 EventHandlers 是否有不同之处,或者基本上具有相同的界面。 一个有趣的学习点是命令和事件可以不同,例如说你有一个 CheckoutCartCommand,事件可能比命令有更多的数据,也可能有很多命令。强烈建议您看看github.com/MarkNijhof/Fohjin 和github.com/gregoryyoung/m-r 关于您的示例,信封通常在外面,而不是在里面(例如肥皂信封)。而且我认为缺少一个属性名称(有效负载?)。 @Yves:好吧,鉴于Something evelope 包含命令必不可少的信息(如果这是客户创建想想电子邮件)我会觉得这很奇怪,你不觉得吗? 我认为您没有掌握信封的概念。如果您有特定于命令的内容,则将它们作为有效负载或标头(带外)放入命令中。但不要将有效负载/标头称为信封。【参考方案6】:除了上面提到的概念上的差异,我认为还有一个与常见实现相关的差异:
事件通常在需要轮询事件队列的后台循环中处理。任何有兴趣对事件采取行动的一方通常都可以注册一个回调,该回调会作为事件队列处理的结果而被调用。所以一个事件可能是一对多的。
命令可能不需要以这种方式处理。命令的发起者通常可以访问命令的预期执行者。例如,这可以是执行者的消息队列的形式。因此,一个命令只针对单个实体。
【讨论】:
【参考方案7】:我认为 quentin-santin 的回答需要补充的是他们:
将请求封装为对象,从而让您参数化 具有不同请求、队列或日志请求以及支持的客户端 可撤消的操作。
Source.
【讨论】:
【参考方案8】:您无法根据命令重新计算状态,因为通常它们每次处理时都会产生不同的结果。
例如,想象一个GenerateRandomNumber
命令。每次调用它都会产生一个不同的随机数 X。因此,如果你的状态取决于这个数字,那么每次你从命令历史中重新计算你的状态时,你都会得到一个不同的状态。
事件解决了这个问题。当您执行命令时,它会产生一系列事件,这些事件代表命令执行的结果。例如,GenerateRandomNumber
命令可以生成一个记录生成的随机数的GeneratedNumber(X)
事件。现在,如果您从事件日志中重新计算您的状态,您将始终获得相同的状态,因为您将始终使用由特定命令执行生成的相同数字。
换句话说,命令是具有副作用的函数,事件记录了特定命令执行的结果。
注意: 您仍然可以记录命令历史记录以进行审计或调试。关键是要重新计算状态,您使用事件的历史,而不是命令的历史。
【讨论】:
【参考方案9】:只是为了添加这些很棒的答案。我想指出耦合方面的差异。
命令指向特定的处理器。因此,命令发起者和处理器存在某种程度的依赖/耦合。
例如,UserService
在创建新用户时向EmailService
发送“发送电子邮件”命令。
UserService
知道它需要EmailService
,这已经是耦合的事实。如果EmailService
更改其API 架构或出现故障,则会直接影响UserService
函数。
事件不针对任何特定的事件处理程序。因此,事件发布者变得松散耦合。它不关心什么服务使用它的事件。一个 Event 有 0 个消费者也是有效的。
例如,UserService
在创建新用户时会发布“用户创建事件”。 EmailService
可能会使用该事件并向用户发送电子邮件。
这里UserService
不知道EmailService
。它们是完全解耦的。如果EmailService
出现故障,或者业务规则发生变化,我们只需要编辑EmailService
这两种方法都有优点。纯粹的事件驱动架构设计更难跟踪,因为它耦合太松散,尤其是在大型系统上。并且命令重架构具有高度耦合。因此,良好的平衡是理想的。
希望这是有道理的。
【讨论】:
以上是关于为啥要分别表示命令和事件?的主要内容,如果未能解决你的问题,请参考以下文章