八年实战经验,解读DDD思想内核

全文4515字,阅读约需13分钟,帮我划重点

划重点

01作者分析了DDD思想内核,强调面向领域的设计方法,保护代码边界,让模型回归业务本质。

02DDD通过限界上下文(BC)降低建模和实现的复杂度,避免领域层被污染。

03然而,DDD在推广过程中面临阻力,如认知陷阱和稳固的知识体系被打破。

04作者建议从参与者、模型、风险隔离和组织架构等方面进行架构设计,以提高系统稳定性。

以上内容由腾讯混元大模型生成,仅供参考

作者:louis,作者公众号:支付进阶之路

我们在聊DDD的时候到底在说什么?DDD为什么这么难落地?8年DDD实战经验,4700字带你解读。

前言

用过DDD的同学会知道,自己用倒没什么,一旦到了要推广部门内使用的时候就会非常困难。为什么会这样?为一个有8年DDD实战经验的过来人,我想尝试分析一下原因:

  1. 过多的概念让人望而生畏,这也是DDD饱受诟病的地方,在推广DDD的过程中我不止一次听到“DDD太复杂了,在我们这里没法落地”。
  2. 也是由于初始学习曲线过于陡峭,大部分人其实处于“懂”和“不懂”中间,这其实很可怕,人们在引入DDD的时候会只引入容易理解的部分,忽略掉自认为“没啥用”的部分,最后导致既没达到想要的效果又增加了复杂度,然后得出一个结论:DDD我用过,不好用
  3. 思维惯性和怕承担风险。人是倾向于使用已有的知识结构去解决问题的,用简单的分层架构,加一些存储和中间件,不用DDD好像也能解决问题。“又不是不能用”,大家都不愿意冒着风险引入一些新东西。

行吧,那我们姑且撇开那些枯燥的概念,只一篇文章也聊不明白(当然我也得假设你至少听说过这些概念,否则你不会点进来看这篇文章)。接下来我们就只探讨DDD的思想内核,或许能稍稍降低一些理解门槛,尽量让大家明白“哦,原来DDD是干这个的!”


DDD的内核是什么

那么DDD的内核是什么呢?

  1. 指导写代码?显然不准确,格局低了一些。
  2. 指导做架构设计?说对了一半,没讲到精髓。

先来看一张架构图示例:

图片

这是现在很典型的一种架构分层,横向先根据不同技术领域先分几层,大差不差也不会有什么人挑战。问题出在应用层和微服务层,实际上很少人能说清这两层分类的标准是什么?高内聚低耦合吗?就是这句看起来说了但又什么都没说的话在指导着架构设计。有没有更靠谱一点的方法论?这就是DDD想说的第一件事,它提供了一种给“应用层”或“服务层”分类的方法

接下来DDD想告诉你第二件事:保护好你的代码边界,否则它会变得腐化且难以维护。这句看起来理所当然的话事实上并没有那么容易做到,首先“边界”并不那么好界定,其次为了守住边界就得立一些规范,有时候为了赶项目进度,边界是可以模糊的,要知道一旦模糊了以后再想规范起来可就没那么容易了。

保护好边界后,面对边界内的核心代码,接下来就是DDD想说的第三件事:让模型回归业务的本质

以上就是DDD内核的全部内容,接下来我想调换一下顺序,先聊一聊看起来最抽象的第三件事。


问题空间和解空间

正式开始之前,我想先聊一聊问题空间和解空间。

问一个问题,把小球向斜上方30°以3m/s的速度扔出,小球多久能落地?小球落地时扔了多远?这是一道初中物理题,为了解这个问题,我们需要进行建模,忽略空气阻力的情况下小球会呈抛物线运动,结合三角函数和动力学方程,很容易能得出问题的解。(没算错的话大约是0.2s,扔的距离大约是0.4m)

可以看到问题空间到解空间的过程需要经过一些抽象,”抛物线“就是一个抽象,我们还要忽略空气阻力,暂不考虑扔球者的身高,合理的建模就能得到问题的解。然而解空间永远不可能等同于问题空间,只可能无限接近。比如你无法模拟出小球运行过程中的空气阻力,也不会为了算一道题就去测量常量g的数值,一般取9.8m/s²。

图片

如果解空间的建模与问题空间相差较大(上右图),也就是说引入了问题空间以外的因素,例如抛球时人手心有没有冒汗,当天的空气湿度之类的。也许问题依然可解,但那会凭空增加许多复杂度。


让模型回归业务本质

在软件领域,问题空间就是业务规则,解空间就是你所建立的系统,你通过建立系统把这个世界的一部分规则通过数字化的方式运行起来。我们理想的方式是解空间是问题空间的一个子集,尽量不引入问题空间以外的因素,这就是让模型回归业务本质的含义。

怎样才能回归本质呢?DDD说要对领域进行建模,所谓的领域(Domain),大白话就是业务规则和业务概念,领域模型就是业务规则的集合。完整的领域模型应该是要包含服务(Service)、事件(Event)和实体(Entity)、DP(约等于ValueObject)等

  1. 服务说的是接口和流程
  2. 事件说的是状态变化或执行结果
  3. 实体和值对象是领域对象的载体

建模的过程大家应该不陌生,就是用UML工具把上面这些东西画出来,现在问题在于怎样让模型更接近业务的本质?DDD提供了一些方法和辅助工具:

  1. 让领域专家参与建模(对这个业务最了解的人)
  2. 建立统一语言(Ubiquitous Language),消除认知差异
  3. 用业务行为来描述接口,不要用技术概念。以账户为例,开户、销户、冻结、解冻、修改注册信息、修改支付限额等都是业务行为,增删改查就属于技术概念
  4. 使用充血模型
  5. 使用DP(Domain Primitive)而不是语言的基本类型。例如电话号码不要用String而用PhoneNumber
  6. 还有一些建模方法为辅助:四色建模法、事件风暴等

这些方法有些听起来挺抽象,但只要回归到原点:让模型尽量还原业务,你就不会再纠结于这些概念。我们目标是建立一个能让人更易于理解的系统,我觉得评判标准也很简单,如果建立的模型在无需额外解释的情况下,技术能看懂、产品能看懂、业务专家也能看懂且各方达成一致,这个模型就是成功的。


保护代码的边界

上面说到业务模型要回归到业务本质,那么非业务的部分怎么处理?我们总要做分库分表、总要引入中间件,以及为了性能考虑经常需要做字段冗余。

这就是DDD强调的边界之一:技术代码和领域模型的边界。

这一边界的重要性不言而喻,我们一定不会希望有一天因技术架构的升级导致业务模型大改,在理想的情况下,业务模型和技术架构的演进是正交的。什么是正交?就像直角坐标系的x轴和y轴那样,分别朝不同的方向延伸,互不影响。

要做到这一点,我们就要合理地使用依赖倒置(Dependence Inversion),把技术细节封装在基础设施层(Infrastructure Layer)。例如数据存储就离不开对数据库的依赖,这时候如果让领域层直接依赖数据库层就会让数据库的技术特性(分库分表、事务、索引等)影响领域模型,这就导致了边界被打破,引入repository层就能解决这个问题:

图片

我们的系统一定是由许多子系统或服务组成,子系统之间会有联系和依赖,有依赖就会有边界,这就是DDD说的边界之二:业务上下游的边界。

DDD为了更好地聊边界,直接引入一个新名词叫限界上下文(BC:Bounded Context),实在理解不了就把BC当成一个系统或服务就好了,BC之间的关系叫上下文映射(Context Mapping),上下文映射有很多种,篇幅原因就讲最常见的两种。

第一种:为了能更好地了解BC之间的协作关系,首先得区分上游(upstream)和下游(downstream),就像水流一样,上游的水脏了就会影响下游的水,怎样能降低影响呢?在中间建一个过滤网不就好了,这个过滤网就是防腐层(ACL:Anticorruption Layer)

图片

注意区分上下游关系,受伤的总是下游,因此ACL也建在下游。通过ACL把上游传递过来的数据转化为下游可理解的模型,这样可以降低上游变化对下游核心模型产生直接的影响。

第二种比较常见的模式,两个BC之间有共享的部分,叫做共享内核(Shared Kernel)。这种情况下可以把共享内核抽出来单独做一个小型的BC,为啥要小型呢?因为这种模式存在一定风险,共享内核的修改会影响多个下游。

图片

现在来想一想,你是否守住了自己的代码边界?可以评估一下,如果说下面这些事项都不需要侵入或改动你的核心模型就能实现,那就可以说代码边界是比较稳固的。

  1. 分库分表规则变化,原来分10张表已经不够用了,需要扩展到100张表
  2. 跑批框架需要从单机扩展到分布式
  3. 为了做高可用,数据通常分布在多中心,要做数据路由
  4. 上游说提供给你的字段规则有所调整,原来是取值0和1,要改为A和B

“分类”的哲学

现实里在程序员之间经常听到这样的争论:

  1. 我们的系统变得太复杂了,要拆分,应该拆成2个子系统还是3个子系统?
  2. 这个接口不应该写在我这里,应该在你那里

实际上这都是在说一件事,就是如何做系统和功能的划分。我认为架构设计到最后就是在做分类:怎么把内聚的功能划分在一起,同时又要考虑把风险隔离开。如果你要问一个架构师怎么做架构设计,也许他会嘿嘿一笑:“高内聚低耦合”。确实架构设计某种程度上是比较主观的,但也并不完全没有方法论可循,DDD在这方面就提供了一些参考。

- 从参与者出发思考

做过用例(Use Case)分析的都知道,参与者(Actor)是与系统交互的主体,参与者很大程度上决定了用例设计的方向,例如同样都是“查询交易记录”,从普通用户角度和从客服运营的角度出发去设计可能会差异巨大。因此按照参与者的不同来拆分可能会是个不错的选择,这样可以避免不同参与者的“利益”相互影响。

图片

- 从模型角度去思考

我们在设计模型的时候,“类图”尤其好用,类图的元素比较全,既包含服务又包含事件和实体(如果你画的是完整类图的话)。从图上很容易能识别模型的疏远近,这时候高内聚低耦合已经被具象化了,再去划分会变得so easy!

图片

(随便画个示意图,大家能理解意思就行~)

- 从风险隔离角度去思考

在业务规模不大的情况下,或许这个因素会显得没那么重要。一旦业务规模上升到一定量级,“风险隔离”在架构设计中会越发占据更多的比重。

举一个电商业务的例子,在电商平台买东西的流程中,支付、扣减库存、生成订单这些环节应该都属于促成交易的“主链路”上,而生成物流单、支付成功后加积分这些环节就不在主链路,特别是在双11这样的高并发场景下,你一定不愿意看到因用户加积分这个功能挂掉导致用户整个交易失败。

因此在必要的时候,给每个功能标记一下风险级别,这也是一种分类的办法。

- 从组织架构思考

“康威定律”在IT界一定不陌生,这个定律揭示着组织架构与系统架构之间存在着微妙联系。我对这一条定律有深刻的体会,因为我曾在这样两种截然不同的组织架构下工作过

a. 在一级部门下先划分业务部门、产品部门、技术部门,技术部门下面再划分不的业务方向。这种组织架构下技术部门有较高自治权,容易孵化出各种中台,技术选型和技术路线决策也会更加统一,人们会倾向于去思考“全局最优解”,但在实施业务战略的时候,需要更加费点劲才能形成组织合力

b. 先划分“事业线”,也就是业务方向,在事业线内部再划分业务部门、产品部门、技术部门。这种组织架构下每个业务各自为战,优先保证的是自己活下来,机动性强,但缺少全局统筹,技术架构通常是割裂的,一直在重复造轮子

不同的组织架构将会直接影响这个组织的思考和运作方式,在做架构设计的时候,技术架构不要试图去和组织架构做对抗,而是要协同。

我在阐述这一节的时候尽量避免用DDD本身的术语,毕竟我是希望透过现象看本质。但如果你看到这里已经对DDD产生了兴趣,不妨可以再查阅资料深入研究一下限界上下文(BC),上面所说的这些实际上都是在说BC,可以毫不夸张地说,BC是整个DDD最核心的一个概念(同时也最不好理解- -||)


总结

现在再来看这张DDD的典型架构图,你可能会更加理解DDD的思想内核,这是一种面向领域的设计方法,把领域层(业务规则)包在最里面,保护好边界,避免领域层被污染。DDD通过这样的方式降低建模和实现的复杂度。

图片

这里或许会有人要反驳了:我就是喜欢用数据建模的方式,CRUD简简单单,我的系统跑了这么多年了,支撑了几百亿的业务也没什么问题。

我会这样回答你,软件工程的解空间通常不止一个,问题不在于”是否能解“而是在于”解的复杂性“,你的建模越贴近业务(问题域),你的解法将会越贴近最优解。

事实上这是我认为DDD推行的最大阻力,这里面存在一个认知陷阱:当你有一套理论能解决现有问题时,你很难接受一个新的理论且那个理论好像更有道理,这意味着你之前长期坚持的稳固的知识体系要被打破,而且你越牛,这个陷阱就越深。

就连爱因斯坦都没能逃出这个设定。我们现在已经知道了在量子态下粒子会呈现出一种概率分布,粒子的动量和位置无法同时测量(海森堡不确定性原理)。但在量子力学的发展史上,爱因斯坦觉得这个理论挑战了他的知识体系,从而诞生了一句名言“上帝不会掷骰子”,他的后半生都在尝试寻找这一套理论的漏洞。