因此,我在聚合根上触发命令,并且由于该命令而发生了大约10个事件。这个事件是内部事件,由于外部系统需要聚合这些事件,我决定进行投影(基本上是读取投影)。为了从10个事件(内部)到1个事件(外部)进行此投影,我必须应用一些业务规则(有关事件合并的业务规则)。我应该把这个规则放在哪里,因为它似乎是域的一部分,但我正在创建内部事件的预测?
基本上,由于投影逻辑是域的一部分,我应该将它保存在聚合中并在进行投影的代码中调用它吗?
更新
所以,在一个聚合根中,我有例如3个事件(内部)作为对一个命令的响应(aggregate.createPaintandwashatsametime(id,red)),它被发送到聚合根并且遍布所有聚合根实体,如:CarCreated(Id),CarSeatColored(Red),CarWashed( )等等(所有这三个事件都是因为单一命令而发生的)。外部系统期望接收一个外部事件作为CarMaintainenceDone(Id,重绘= true,wash = true,somevalue = 22);
现在,如果我有一些复杂的逻辑来制作这个CarMaintainenceDone事件(比如if(color == red然后在投影中,somevalue == 22,否则为44) - 这应该是投影代码还是域的一部分?
更新2
让我试着给你一个新的例子。只需忽略域的建模方式,因为这只是一个例子:
正如您所看到的,我们有包含Multiplier的AggregateRoot,只是用正确的名称来调用它。当我们进行乘法时,我们首先将整数1发送给ObjectA,它有一些逻辑来设置内部状态并发出ObjectAHasSetParam事件。 ObjectB也是如此。最后,ObjectC侦听所有这些事件,而paramsHasBeenSet将执行实际的乘法。
在这种情况下的事件存储中,我会保留事件列表:
[ObjectAHasSetParam , ObjectBHasSetParam , ObjectCHasMultiplied ]
我的观点是:如果我一个接一个地发出所有这些事件 - 其他人更新的状态可能会不一致,因为这3个事件只有在一起才有意义。这就是为什么我想制作类似投影的东西,但我认为在这种情况下我只需要一起发布这些事件的列表而不是事件。
class AggregateRoot{
Multiplier ml;
void handle(MultiplyCommand(1,2)){
ml.multiply(1,2);
}
}
class Multiplier{
ObjectA a;
ObjectB b;
ObjectC res;
void multiply(1,2){
a.setParam(1);
b.setParam(2);
publish(paramsHaveBeenSet());
}
}
class ObjectA{
int p;
void setParam(1){
p = 1 + 11;
publish(ObjectAHasSetParam(12));
}
}
class ObjectB{
int p;
void setParam(2){
p = 2 + 22;
publish(ObjectBHasSetParam(24));
}
}
class ObjectC{
int p1; int p2;
int res;
listen(ObjectAHasSetParam e1){
p1 = e1.par;
}
listen(ObjectBHasSetParam e2){
p2 = e2.par;
}
listen(paramsHaveBeenSet e3){
res = p1 * p2;
publish(ObjectCHasMultiplied(288));
}
}
答案 0 :(得分:1)
哈哈!简短的回答是流程经理。外部系统期望接收一个外部事件,如CarMaintainenceDone(Id,重绘= true,wash = true,somevalue = 22);
答案越长,你(应该)现在就有两个聚合。其中一个是跟踪汽车的状态。另一个是跟踪维护汽车的过程。
在某个地方隐藏了另一个聚合的大提示:您已经有了这个CarMaintenanceDone
事件,没有聚合负责生成它。所有事件都有"聚合"某个地方生产它们。聚合可能是现实世界,也可能是现实世界的代理(HttpRequestReceived
),或者是其他有限环境中的数字事物;但事件告诉你,某个地方的某些东西改变了状态。
也就是说,您有一些知道维护完成时间的规则的聚合。它是一种信息资源,一份工作日志。当CarWashed
发布时(由Car,或洗衣机或其他任何人发布),订阅CarWashed
事件的事件处理程序会向维护聚合发送命令以通知它。维护聚合更新其自己的状态,运行其逻辑,并在已考虑所有单个步骤时发布MaintenanceCompleted
事件。
大多数流程都可以作为聚合实现;奇怪的是,"命令"往往看起来像事件处理程序。但是他们有自己的历史(基于他们所观察到的),它描述了状态机如何根据观察到的每个事件而发生变化。
可能超过两个,具体取决于流程的复杂程度。
Rinat Abdullin写了一篇很好的introduction to process managers,我经常参考。
虽然聚合和流程经理之间有明显的区别吗?我认为流程管理员只会协调并生活在应用服务领域,根据收到的事件向聚合发送适当的命令。
从我所见过的 - 不,没有。文献并没有说清楚。
这是我能给你的最强烈的指示,你知道你正确地做了CQRS:你的根源是传奇。
佐贺,在这里,相当于一个过程。
答案 1 :(得分:0)
不应该有外部事件的概念。事件由聚合生成,并由同步读取模型,传奇或者发布到外部世界使用,其他系统和微服务可以随意使用它们。
因此,在您的情况下,消费者(例如,作为saga实现)应该按照其业务规则聚合这些事件,然后执行某些操作(例如,saga可以创建新命令)而不是聚合。
更新(回复问题更新)
如果您认为汽车维护是Car
汇总的责任,那么Car
汇总应该引发此事件。这取决于Car
聚合的未来行为如何受CarMaintainenceDone
事件的影响。在这个特定的上下文中,我将从Car
聚合生成事件,以使代码更简单。
答案 2 :(得分:0)
通常有2个事件模型,内部事件(仅在BC中可见)和外部事件(发布到外部世界)。您可以决定将所有内容都设为外部,但是您必须对所有内容进行版本化。
您可以在Patterns, Principles, and Practices of Domain-Driven Design book p.408中阅读有关内部与外部事件的更多信息(在链接中向上滚动)。
预测不应负责发布外部事件。一种常见做法是从应用程序服务层注册内部事件处理程序,该应用程序服务层负责在消息传递基础结构上发布外部事件。您可以利用该过程将这些事件聚合在一起,并从中发布单个外部事件。
如何执行聚合将取决于您,但由于内部事件可以同步引发并且处理程序通常是单线程的,因此您可以在处理程序中设置一个状态机,当它收到第一个事件时启动批处理并聚合它们直到它收到最后一个,然后在消息总线上发布。
如果您的消息传递基础结构无法参与与事件存储相同的事务,您可以只有一个额外的进程按顺序读取已提交的事件,并执行与上面相同的操作。
另一种方法是让消费者处理聚合。如果消费者能够否决" CarMaintenanceDone"那么这可能是正确的方法。装置
最后,您还可以从聚合本身发布一个额外的事件。 AR本身可能无法利用该事件,但有时候做更实际的事情会更好(就像用读取模型消耗的数据来丰富事件一样)。如果添加更多事件,这种方法还具有不必更改逻辑的优点。