DDD,CQRS和事件溯源这十年 - Tacta


DDD带给了我们(包括我)很多软件开发的乐趣。当你能够领域分解分析时,后面的实施就变得容易了,它会导致一个简单,可维护且易于理解的代码,将比开发团队本身更长久。
自DDD发布“蓝皮书”以来,DDD已经走过了漫长的道路,但根据我的经验,没有多少人意识到DDD是一个不断发展的,不断拓展的东东,并且它确实学到了一些新的技巧。本博客简要回顾了DDD社区在过去十年左右发生的一些值得注意的事情。

我能够DDD吗?
首先要做的事情。您需要什么才能成功实施DDD?它的先决条件是什么?

根据DDD作者Eric Evans的说法,需要成功实施DDD主要有两个条件:

  • 迭代开发过程
  • 访问领域专家

迭代开发是DDD的生命力量。它可以实现实验,探索和调用问题域的主动重构,因为您可以继续与领域专家一起获得更多洞察力。此外,您正在使用严格的反馈回路,其中包含通过实际使用正在构建的系统获得的反馈。

当你首次想建模时,你能够一次就发现适合领域的“最佳”模型的概率有多少?机会很渺茫。尤其是因为几乎任何你可能正在建模的东西都没有“完美”的模型。

领域模型是一种生物(有生命的),它通过主动发现随着时间的推移而增长和发展,并且从未真正完成。它最多也就只能“足够好”。这就完全没问题了。

我们所做的重大错误之一就是滑向完美主义。正如我们已经明确指出的那样,DDD依赖于迭代,因此不要过早地处理细节。快速完成第一个原型,然后快速进入第二个原型......

“完美是善的敌人Perfect is the enemy of good”,完美主义阻止你做足够的创新。

话虽如此,不要满足于您可能遇到的第一个有用的模型。保持迭代并始终​​严格完善并持续观察即使是最无辜的外观变通方法。它们虽然并非最佳模型,几乎可以肯定的是,你会错过了一路上的建模机会。

迭代开发方法中需要大量使用领域专家。两者相辅相成,并且错过任何一个肯定会使你的DDD工作徒劳无功。

在这一点上,您可能会想:“嗯,DDD lite怎么样?”它为我提供了许多有用的抽象和建模工具。我不能仅仅使用DDD lite方法提供的工具和抽象来逃避?

对不起,但答案是肯定的。由于其性质,DDD Lite只能让你到目前为止停止了。它提供了一组建模工具/构建块(例如实体,存储库,值对象等......),它们将帮助您实现DDD本身!只使用DDD lite,你实际上完全错过了DDD ......

所有的抽象和建模工具都有其用处,如果你不清楚你正在构建什么,或者甚至更糟......如果你正在构建错误的东西呢?

显式上下文边界和核心域
正如Eric所说,专注于核心领域是一个改变游戏规则的游戏。
将您的DDD工作重点放在核心域上。真正让你的公司在竞争中脱颖而出的东西。在市场上为您提供优势和竞争优势的东西。
公司可以通过重新发明轮子并将DDD应用到他们领域中的一部分,但这会浪费很多时间,金钱和精力,这些部分本来可以通过更简单的方法获得,甚至可以被现有的现成解决方案所取代。
但是,为了识别您真正的核心域,您需要通过任何上下文映射技术定义显式上下文边界(我建议您查看事件风暴,稍后会详细介绍它......)。
说到这一切,需要一定程度的纪律来保持有界上下文之间的分离,但它产生了很大的好处,几乎任何项目,无论是否使用DDD,无论大小,都可以从上下文映射和明确定义了上下文边界,它将域中真正重要的部分与不太重要的部分区分开来,最终将帮助您识别您的核心域,而这正是您需要针对大部分DDD工作的地方。

上下文映射和泥球
如果你正在处理一个遗留系统,一个泥泞的大球,你会怎么做?你如何在那里尝试DDD(假设你仍然可以采用迭代开发方法,并且可以访问领域专家)?
这并不意味着你必须用新的功能来控制它,而是在这里使用你的上下文映射技术。在它周围划一条线,然后说:“这是我的大泥球”,之后在你的新服务周围划一条线,并将其视为一个独立的有界环境。
正如Eric所说的那样,你的服务最终可能不可避免地被泥球所包围(因为它几乎可以吃掉任何东西),但至少你有一段时间的好运。

关于DDD构建块这个词
让我们再次快速触及DDD lite:

构件(实体,价值对象,工厂,存储库......)已经过分强调啦! - eric.evans

是的,你听到了。构建方式已经得到了太多关注,但不要误解我们,它们仍然很重要并且提供了很大的价值。构建块就是它们的本质。它们是达到目的的手段,仅仅是帮助您实施战略性DDD模式的实施细节。

正如埃文斯所说,他遗憾地将战略模式放回到本书的最后,这可能导致许多人更加重视战术模式,因为他们是第一个甚至更糟,他们陷入了错综复杂的实施细节在战术模式中,他们甚至没有达到本书最重要的部分。

需要考虑的是构建块为您提供了实施DDD的工具,但您应该更多地关注战略模式,更重要的是因为战术模式/构建模块将继续发展,有些将变得过时,新的将变得过时被添加(例如,域事件)。

Aggregates聚合
聚合表示域中的概念整体,其也由其他小部分(值对象和/或其他实体)组成,并保护所有这些部分的始终一致的不变量。
经常出现的一个问题是许多人对于他们的聚合具有跨越数千个实体的不变量的尴尬案例的担忧。
由于我们只能通过其父聚合访问子实体,我们是否每次加载聚合时都加载所有子实体?我们是否使用延迟加载?我们是否以不同的方式对此进行建模,即使它是业务不变的?如果是的话,这是什么样的正确模型?
OO真的不擅长处理对象集合,特别是非常大的集合,这些集合要求在这些情况下稍微松动和“弯曲”规则。
如果您具有少量聚合但具有许多可能同时与聚合交互的并发用户,则同样适用。
在这些情况下,您可以考虑将这些实体建模为单独的聚合,并尝试在更高级别上强制执行业务不变量。例如,在域服务,流程管理器/Saga等中......  简而言之,使一致性成为您域的明确关注点,而不是通过基础架构隐式解决。
事实证明,另一个潜在的解决方案与另一个问题/关注点有关,该问题/关注点在使用CQRS作为独立模式或与Event Sourcing结合使用的项目时不断涌现。

可以写边查询读边吗?
根据Greg Young的说法,答案是“ 是的,绝对的,有些情况下你只需要”,其中一个案例与我们在这里提到的挑战有关。
我认为,即使你没有面对我们在这里描述的聚合/实体问题,也有很多情况下可以查询读取方(我肯定已经多次完成)。
发现这些机会相当容易,因为在大多数(如果不是全部)情况下,他们倾向于以规范specification 模式的形式呈现自己,但由于您使用事件溯源或CQRS作为独立模式,您实际上不可能真的使用它们。 

但幸运的是,规格specification和CQRS是两种竞争/可互换的模式。
经验法则是瞄准很小很具体的预测,可为您提供非常具体的回答,可通过查询领域模型来实现的问题。

Vaughn Vernon对聚合设计有很多话要说。查看他的两部分聚合设计论文:- 有效的总体设计第一部分有效的总体设计第二部分
我还强烈建议您查看他的DDD书:实现域驱动设计

DDD处于现代“永远在线”的世界
通常,企业世界之外的软件应用程序在性能,延迟,响应性和可伸缩性方面具有完全不同的要求。由于前面提到的限制以及应用DDD的OOP引入的开销,人们担心将DDD模式应用于这些类型的域并不可行。
这可能是DDD广泛采用的一大障碍,但幸运的是,将DDD应用于这些类型的领域,在Event Sourcing和CQRS的名义下应用DDD产生了一种新的方法(这些想法实际上存在了几个世纪)。
简而言之,事件溯源及其互补模式,CQRS为我们提供了DDD构建模块的奢侈改造,以及通过使用领域事件作为一等公民来使用DDD模式的方法,以及这些系统中唯一的事实来源同时允许我们满足这些类型系统的高吞吐量/高可用性需求,同时允许我们单独扩展读取和写入,并通过领域事件使用服务间集成。
由于事件流的不可变性(事件状态是其所有事件的左侧部分),事件溯源还为在函数世界中实现DDD提供了踏脚石。
但是,满足大型分布式系统的可扩展性需求并不是采用这种以事件为中心的方法来建模我们的系统的一个唯一的好处。

采用事件优先建模方法还有许多其他好处:

  • 明确地对重要域事件进行建模并将其形式化,迫使域专家根据其系统行为而不是结构进行思考。这尤其有用,因为人们往往会考虑他们的遗留系统,而不是专注于他们真正想要解决的问题。
  • 通过对事件进行建模,并专注于行为动词而不是名词,甚至领域专家也可以对他们的领域有不同的看法并获得额外的洞察力。
  • 事件建模迫使时间聚焦,并使时间成为一个关键因素(它是)

如果其中任何一个与您产生共鸣,我建议您查看Alberto Brandolini的事件风暴 。Event Storming采用以事件为中心的方法来提炼领域。

我不会在这里详细介绍,但我只想提一下,Event Storming在网站和书中会有很多不同的风格,但我想提一下Greg Young的另一个变种。
在他的变体中,您基本上只需将一个长期运行的业务流程端到端地、并使用Event Storming对其进行建模,以发现您的服务边界。这对我很有用。

事件溯源/ CQRS误解/痛点
在他的一次会谈中,Young专注于一些反复出现的关于事件溯源和CQRS的痛点/误解,并就如何处理这些问题提供了澄清和建议....... 这是一个简短的回顾。

CQRS不是顶级架构!
CQRS是一种支持模式,您需要对其进行处理。别发疯了!相反,将它有选择地应用到几个地方。

命令必须返回void?!
底线不是关于返回的值,而是关于副作用?命令返回错误例外是完全正常的,而不是仅仅依赖于抛出异常(这无论如何都是一种不好的做法)。

命令与领域事件不是严格的一对一关系!
事件不一定具有相应的命令,命令也没有恰好地会发布一个相应的事件,重要的是要理解总有两组用例:进来的是命令和即将发布出去的是事件。
事件并非严格来说是一个命令进入的结果。在以事件为中心的体系结构中,这是常见的,并且它的全部意义在于事件导致事件发生。导致发布域事件的业务流程通常由另一个没有任何命令的事件触发。

没有单向/异步命令这样的东西
命令的全部意义在于我有能力告诉你不!
“异步”命令并没有真正给你这个选项,你只是发射了并忘记,这有点不符合我们的目的。它们在现实世界中效果不佳。
通过接受命令,它应该意味着您验证了它,您可以执行它,您处理它并且它已经完成。否则,您真正想要的是下游事件处理器。

不要编写CQRS /事件溯源框架
我想我们需要开始意识到你不需要一个框架来解决所有问题。框架有其自己的位置,但我们需要结束框架优先思维模式,而是尝试用重点模块/库来解决我们的问题,而不是依赖于几乎总是“太通用”的框架,这些框架往往会在我们的整个框架上蔓延代码库。

我们需要更好的例子!
正如Greg所说,我们需要更好的例子。事件溯源很难,这是事实,像简化购物卡这样的简单例子并不能真正做到这一点。

原文点击标题进入