观《if (domain logic) then CQRS, or Saga?》所悟

引言

Udi Dahan曾在2017年阿姆斯特丹的DDD欧洲年会上发表过一篇演讲——if (domain logic) then CQRS, or Saga。视频是UP主从Youtube搬运的,我听力水平一般,所以以下内容有所偏颇的话,还请见谅。

在演讲中,他提到了Sandbox、Private Domain、Public Domain和Collaboration Domain等一些概念,为更好地应用DDD开辟了不同视角。以下便是我的思考与收获。

正文

Udi用级联删除的例子,引出了沙盒Sandbox。错误的级联删除操作,特别是在数据库中Table级别上的删除操作,对任何一个系统而言都可谓灭顶之灾。所以顺理成章的,我们会使用给数据行打上某种删除标志的方法,称之为“软删除”,避免数据被彻底删除。这就象操作系统里的回收站一样,给了我们一次反悔的机会。如果在所有的软件系统里都有这样一个安全沙盒,是不是就完全解决了删除问题?

  • 对此,我认为,对于系统中的所有删除操作,首先都应该有领域行为上的具体含义,比如解雇员工、取消订单、停产产品等等。其次,还要考虑受到删除操作影响的数据,要限定在哪个范围,它们是否有留存下来的业务价值。比如解雇员工后,与之相关的薪酬记录就不能删除,否则会导致账面错误。反之,只在薪酬记录里有个员工ID,却查不到这名员工的姓名、职务等具体信息,也会发生错误。再或者,这名员工先被裁员,之后在招聘时被优先聘用时,如何处理其原有信息,等等。所以,“删除”操作不能简单论之,必须是从业务领域的角度去思考和命名——其究竟是何种业务操作,这种操作在业务上应导致何种后果。比如雇员被解雇后,其所有数据被冻结,其雇员信息不再会更新,其薪酬不会再发放。

随后,Udi用博客作为例子,引出了Private DomainPublic Domain的概念,并阐述了Sandbox与二者之间的联系。一篇随笔被正式发布前,博主可以在编辑页面随便折腾。而在随笔正式发布后,所有的修改和删除操作就需要慎重了。此处,发布前的随笔处于Private Domain,发布后则进入了Public Domain。对处于Private Domain范围内的私有数据,我们可以随意地增删查改,而不用顾忌任何的业务规则和约束。而当私有数据被推送到Public Domain时,则必须顺利通过各种规则的审核与验证,进而成为构成系统的固定组成部分。其中,每个Private Domian对应一个Sandbox,用户可以为所欲为,而不必担心对系统造成实质影响。

  • 对此,我认为,每个Sandbox都对应某个具有领域含义的实体,并可以用与之相关的领域事件作为Private Domain与Public Domain的划分界线。比如一份菜谱,你可以随意修改菜谱里各种菜品的价格、名称,甚至新增或者撤掉某个菜品。而在正式提交更新前,所有人仍旧会使用旧的定价和菜名。这里的菜谱,就是所有菜品所在的Sandbox边界。而最终MenuUpdated事件的触发,才标志着操作对系统产生了实质性改变,数据已经进入Public Domain。如果没有这个提交的步骤,让所有的增删查改都立即产生效果,那顾客一定会开骂了。另一方面,如果没有暂存修改结果的这个Sandbox,也意味着完全不给饭店老板活路——要改就一口气改完,还不能自相矛盾。回到前述的删除操作也是同理。当数据还在Private Domain时,我们可以放心地删除之。而当数据已经进入Public Domain后,就必须去发掘删除操作背后的领域含义了,并由此衍生出更复杂的领域逻辑和领域行为。

Udi接着商品被删除的例子,延伸到购物车中已加入的商品售罄或被停售的情况,提出Collaboration Domain的概念。在竞态条件(即并发条件下的竞争条件)下,即表明有多个参与者需要对同一个数据进行操作,此时属处的领域即Collaboration Domain。这样的协作领域,通常可以围绕if语句进行发掘。有if判断涉及其他的实体,则通常表明该数据也可能会被其他参与者改变。此时,CQRS成为很自然的选择。因为命令执行的环境是经过事先检验的,所以命令总是能成功执行。在这样的设定下,把一个用户下单的操作分割为多个步骤,在放入购物车和提交订单时分别进行一次商品有效性的检验。

  • 对此,我认为,引入CQRS是因为将命令与查询分离后,检验执行环境的部分将由一组查询构成,改变系统状态的部分则由一组命令组成,这样将更容易发现竞态条件,从而做出合理的选择。

为了减少检验的次数,Udi借Shopping Cart Timeout的例子引出了Collaboration Timeout的概念。给购物车一个活动状态的超时设定:用户放入商品时,购物车进入激活状态,跨入协作领域,此时因商品在售,订单可以成功提交;用户未在超时前提交订单的,购物车进入失活状态,退出协作领域,之后商品将因售罄或停售而无法再加入购物车。

  • 对此,我认为,此时的购物车更象是一个愿望清单(Udi在演讲里也有提及),这样的Timeout与超卖都是解决最终一致性条件下的不同解决方案。MSDN CQRS Journey第164页提到的方案,是根据代价大小,选择对客户做出赔偿或者在付款前加锁判定,让其他人等待一小段时间。当席位较多时,因为竞争风险小,可以将席位是否够用的查询交付异步执行,使用户等待时间减少;当席位紧张时,允许提交失败的用户修改订单。唯品会采用的方式,是在商品加入购物车后,将该商品暂时锁定,保证用户在超时前能成功下单。

由于Collaboration Domain和CQRS的存在,Udi指出,必须改变传统的思考方式,因为Saga将更普遍地出现在模型之中。对此不能有退缩和犹豫,应当与领域专家深入交流,使自己也成为业务专家,确保每个流程都完全可控。最后,Udi提出消息中间件是实现Saga的重要工具,于是顺手推介了一下自己维护的NServiceBus框架。

  • 对此,我认为,以用户购物这个Saga为例,将由用户注册->查看商品详情->放入购物车->提交订单->完成支付等多个流程协作完成,而不是简单地放在一个或几个数据库事务里完成。在不同聚合之间以消息或事件为纽带,是处理Saga的核心与关键。所以CQRS、Event Sourcing以及Message Queue都将是实现DDD的利器。
posted @ 2020-12-26 22:56  没头脑的老毕  阅读(377)  评论(0编辑  收藏  举报