情境:
发票的收件人必须是有效的联系人。
CreateInvoiceCommand:
{
"content": "my invoice content",
"recipient": "42"
}
我现在读了很多次,写边(=命令处理程序)不应该调用读取端。
考虑到这一点,Invoices微服务必须监听所有ContactCreated
和ContactDeleted
事件,以便了解给定的收件人ID是否有效。
然后我会在Invoices微服务中拥有数千个联系人,即使我知道其中只有少数会收到发票。
有没有最佳做法来处理这些情况?
答案 0 :(得分:5)
发票的收件人必须是有效的联系人。
所以你需要注意的第一件事 - 如果两个实体是不同聚合的一部分,你就不能真正实现“仅当 实体满足规范时才对这个实体应用更改“,因为 实体可能会在评估规范的那一刻和执行写入的那一刻之间发生变化。
换句话说 - 您只能在聚合边界上获得最终一致性。
聚合是其自身状态的权限,但是其他所有内容(例如,命令消息的内容),它几乎必须接受某些外部权限已检查数据。
您可以采取几种方法
1)您可以盲目地接受命令中指定的收件人有效。
2)您可以尝试从从不受信任的来源接收并将其提交到域之间的某些外部机构(也称为:某些其他聚合的读取模型)验证收件人的有效性模型。
3)您可以盲目接受所述命令,但将发票视为临时发票,直到确认收件人的有效期为止。这意味着在发票上运行第二个命令来验证收件人。
注意 - 从模型的角度来看,这些不同的命令是等价的,但在应用程序层它们不需要 - 您可以将命令的访问权限限制为可靠来源(不要使用它)公共API的一部分,需要只有可靠来源的授权等。)
方法#3是最微观的,因为这两个命令可以及时分开 - 您可以在它到达时立即接受CreateInvoice命令,并异步验证收件人。
在哪里可以使用方法4),Invoices Microservice有自己的联系人存储,只要有ContactCreated或ContactDeleted事件,它就会更新?然后,两个实体都是同一服务和边界的一部分。现在应该可以使事情保持一致,对吗?
没有。您已将这两个实体作为同一服务的一部分,但问题从来就不是它们位于不同的服务中,而是它们位于单独的聚合中 - 这意味着我们可以同时更改实体状态,这意味着我们无法确保它们立即同步。
如果您希望立即保持一致性,则需要一个能够区别对待的模型。
例如,如果发票实体被建模为Contacts聚合的一部分,则聚合可以确保新发票需要有效收件人的不变量 - 域模型使用内存中的状态副本来确认我们加载时收件人 有效,并且写入记录簿会验证自加载发生以来记录簿没有发生变化。
聚合状态的写入是记录簿中的比较和交换;如果某个并发进程使收件人失效,则CAS操作将失败。
当然,权衡是对联系人聚合的任何更改也会导致发票失败;同时使用同一个收件人编辑不同的发票就会消失。
聚合全部或全部;它们是不可分离的。
现在,一个可能是你的发票聚合有一个必须立即与收件人一致的部分,另一部分最终是一致的,甚至是不一致的,是可以接受的。在这种情况下,您的目标是重构模型。
答案 1 :(得分:2)
发票的收件人必须是有效的联系人。
这是一项商业规则。应该问一个问题,这个商业规则对我的申请意味着什么?谁应该对实施这条规则负责,还是可以分担责任?
一种可能性是,是的,业务规则与发票有关,因此发票服务部门应负责实施发票。
但是,业务规则实际上是关于发票的创建。而且,您的架构中创建发票的所有者奇怪地不是发票服务。原因是命令的名称是CreateInvoiceCommand
。
让我们考虑一下 - 发票服务永远不会自己创建发票。它只是提供了这种能力。在此体系结构中,发票创建的实际所有者是命令的发件人。
使用这一推理线,如果业务规则是说无法针对无效收件人创建发票,则命令发件人有责任确保实施此业务规则。
如果发票服务订阅了活动,而不是接收命令,这将是一个非常不同的情况。例如,一个名为WidgetSold
的事件。在这种情况下,发票创建的所有者显然是发票服务,因此业务规则将在那里实施。
如果用户点击联系人42的创建发票按钮,那就是 用户有责任注意联系人42的存在
是的,这是正确的。用户的意图是创建发票。因此,应在此时强制执行发票创建的业务规则。如何发生这种情况(或者是否发生这种情况)是一个不同的问题。
但是如果用户不关心怎么办?然后它会创建一个发票 收件人ID无效。
也正确。正如您所说,这种方法存在副作用,其中之一就是您可能会在整个系统中出现不一致的数据。这是SOA的现实之一。
这不是类似于此:发票有一个currencyCode 属性,它是一个字符串。
我不知道我是否同意。请问这是一个有效的ISO货币吗?与要求实体42是否有效根据另一个系统不同?。我想是的。
如果收件人不是空且有效,那么是否与相同 根据我的联系人数据库?
我同意,实际上,您可以在服务中实现此验证。我只是说我认为它不适合它。如果您想这样做,则必须先调出另一项服务或在本地存储所有联系人,因为您最初构建了问题。我认为在服务之外做这件事更简单。
答案 2 :(得分:1)
我认为答案取决于您希望系统具有多大的弹性,即如何处理Contacts Microservice
关闭(没有响应或非常慢)的情况。
<强> 1。你想要非常有弹性
如果Contacts Microservice
关闭,您希望能够为某些(可能是大多数)联系人发出发票。在这种情况下,您会收听ContactCreated
和ContactDeleted
并维护有效联系人的(最终一致的)本地列表;它们应该在这个有界的语境中相应地命名为无处不在的语言,如Payers
(或类似的东西)。然后,在Application层中,构建CreateInvoiceCommand
时,检查Payer
是否有效并创建命令。
<强> 2。你不需要有弹性
如果Contacts Microservice
已关闭,您拒绝生成发票。在这种情况下,在构建命令时,您向Invoices Microservice
API端点发出请求,并验证Payer
是否有效。
在任何情况下,您都会在发送命令之前检查联系人的有效性。