2015年之后,随着云原生、微服务、大中台等一系列技术名词诞生的同时,还有一个耳熟能详的名词“领域驱动”也开始被捧上神坛。笔者初次听到领域驱动是参加一个技术分享会,当时给我的直观感受就是:好像说了什么,但又好像什么都没说,很多概念很"形而上学",在天空中飘啊飘,无法落地。
十年过去了,中台已经过气,微服务回归单体也一度成为技术圈讨论的热点话题,曾经神坛上云遮雾绕的 DDD 在今天看来是否还有讨论的意义?在过去一两年的实践中,笔者对 DDD 有了更深的体会,本文将阐述我的一些浅见,如果有理解不到位的地方,也希望同学们一起讨论。
01
领域驱动的理念
领域驱动这个概念一开始是由大神 Eric Evans 在2003发布他的名著《Domain Driven Design:Tackling the Complexity in the Heart of Software》中提出,从标题中可以直观的知道 DDD 是为了解决软件系统的复杂性问题,是一种降低业务系统复杂度的方法论,许多同学认为领域驱动难以理解,是因为他提出了很多抽象的概念,我们不妨先抛开这些概念,先去理解一下它去解决问题的思路。
1.1 统一语言与模型
对于一个开发来说,我们的工作一句话就是:用代码实现需求,在实现的过程中不同的人、不同的团队,可以有不同的实践,领域驱动就是其中的一种实现路径。领域驱动在需求到代码之间试图建立起一条桥梁,桥梁的名字叫统一语言和模型。
什么是统一语言?软件开发的核心难度就在于处理隐藏在业务知识中的复杂度,想要处理这种复杂度,首先需要打破业务与技术之间沟通壁垒,在一个项目中,不光是有开发人员,还有测试、运维、产品、pm 等等,能把事做成的前提是可以把事情说清楚,我们知道中华文化博大精深一句话在不同的环境、场合下完全有不同的语意,统一语言的思想就是提倡团队内通过不断沟通去确定在一个业务领域中的术语或概念有唯一明确的语意。
那什么是模型呢?我总结为:一种化繁就简的抽象,抽象是目的是为了简化问题,先忽略细节,从顶层思考问题, 抽象不在乎形式的表达,而取决于如何看待问题和分析问题的角度,我们举几个例子说明:
把大象装进冰箱需要几步?一共三步,打开冰箱->放入大象->关上冰箱。
虽然这是一个梗,但不得不说,这是很好的一种面向过程的抽象。
程序=数据结构+算法。
这已经变成了计算机科学中最基本的一条原则,即任何程序都可以分解为算法+数据结构,虽然程序要解决的问题都没有确定,但是已经有了一个思考问题的方向。
类图、流程图、架构图...
对于一个比较复杂的系统,我们很难通过几句话把它讲清楚,这个时候,画图就成为了一个好的表达方式,而画图就是一种抽象,根据你想抽象角度的不同使用不同类型的图。例如有人让你讲讲购物网站做了什么,你可以把问题简化并用下图表示,即描述用户、商家、平台之间的关系,这正是面向对象的建模思路,而领域驱动本质上也是一种面向对象的建模方法。
在引入统一语言和模型抽象的思路之后,就可以把需求到实现的这个过程用下图表示,技术和业务的相关同学通过统一语言去沟通交流需求,通过模型抽象描述需求,最后按照模型去实现相应的代码,领域驱动的一大目标是:修改需求即修改统一语言,修改统一语言即修改模型,修改模型即修改代码,这也就实现了从需求到代码的有效信息传递。
1.2 分而治之:再谈归并排序
为什么这里要谈起归并算法,因为领域驱动所提倡解决问题的思维方式和归并排序算法如出一辙,可以总结为一句话:自顶向下拆分、自低向上合并。
我们简单回顾一下归并排序的思路:
明确主函数的功能、输入、输出;
分解问题,确定分解后子函数的功能、输入、输出;
合并子函数的返回,伪代码如下:
//主函数void mergeSort(std::vector
而领域驱动在实现的过程中依旧沿用了这个思路即:定义问题、分解问题、合并结果。
定义问题:当我们面对一个复杂场景时,首先需要确定面对的问题是什么?问题的边界(限界上下文)在哪里,我们很容易理解解决问题带来的价值,但是很容易忽略定义问题带来的价值。在项目实践中,不知道你有没有遇到过这样的一种场景:技术同学会根据产品同学的一段描述立马会陷入到技术实现中,等到验收的过程中才会说出:“哦,原来你只需要实现这种需求呀”的感叹,这就是没有找到核心问题所在。
分解问题:定义好问题即明确了问题的边界,我们就可以在边界内进行问题的划分,领域驱动的核心思想是分治,即拆分“边界分明”的子问题,再针对子问题进行解决。这里分解问题的思路,也和微服务拆分的思路有异曲同工之秒(说到微服务这里抛出两个有趣的问题 ,第一:领域驱动是03年就提出来了的概念,为什么一直到15年左右才渐渐被大家熟知;第二:微服务和领域驱动有什么关系,我相信在读完本文后,你心中自有答案)
合并结果:解决完一个一个的子问题还不够,如何把信息串起来才是难点,在串起来的过程中就会出现耦合问题,这就又回到了一个软件实践中一个普遍的问题:如何做到“高内聚、低耦合”,其实细心的小伙伴已经发现了,这里的分解问题的目标就对应着高内聚,而合并结果的目标则是低耦合。
02
那些晦涩难懂的术语
上一节我们讲解了领域驱动所想解决的问题:降低系统复杂度,以及它解决问题的思路:统一语言、模型抽象、分而治之。明确了这两点,我们再去看看其中的一些抽象的概念,相信你会有更深的理解。
2.1 在边界内做事:领域与子域
概念上领域指:从事一种专门活动或者事业的范围,这里的重点在于范围两个字,范围即边界。不管我们去解决什么问题,问题总是有边界的,边界越清晰,解决问题的思路则越清晰,再简单点说,领域就是在一个边界内要解决的问题。针对一个复杂问题,使用分治的思想,还可以进一步拆分成子问题。这种研究问题的思路其实已经司空见惯,假如我们需要研究人体,那么人即问题的研究对象,我们可以按照不同的方法把把问题拆分成子问题,以下是两个不同的思路,左图是按照“系统” 划分,右图是按照“组成”划分。
如何分解,没有所谓的标准答案,拆分的方式不同,其实也可以说是抽象的角度的不同,因为抽象的角度不同,研究的方法也会有不同。比如中医研究人体会侧重于整体和部分的关系,西医则侧重于定量分析,我们不能说那种好或者不好,只是看待问题的角度不同,角度即抽象。
当完成分解过程,我们在针对子问题,再寻求对应的解决思路,这个过程就是从问题域到解决域的过程,以下图可以更直接的帮助你理解。
2.2 领域按功能再划分:核心域、通用域、支持域
在不断划分的过程中,还可以按照功能性的不同把子领域再次的化为:核心域、通用域、支持域,这里需要强调的是子域的划分完全建立在对于业务的理解之上,基于业务,而非技术。
核心域
是指富有竞争力的领域,这里是仁者见仁、智者见智,不同的人对于竞争力有着不同的理解,比如还是拿人来举例,身体、认知、财富到底哪一个是一个人的核心的竞争力,当认为是身体是核心的人就会侧重于锻炼健身;认为认知是核心的人则会侧重于看书学习;认为财富重要的人则会侧重于事业.... 总体看并不能说谁对谁错,这是看待问题的方式不同。
而对于公司也是一样的道理,我们看很多公司的业务和产品,表面上很相似,但是其实有着完全不同的商业模式,就以电商平台举例,有的核心领域在物流服务、高端的品质;有的核心领域则是更加便宜好用的货物上,对于一个公司来说,划定了一个核心领域,其实也就确定了资源投入的方向,把好钢用在刀刃上,提供差异化的价值服务。
通用域
高复用能力或者没有太多个性化需求的领域,比如所谓的“中台”概念就是指高复用的模块化服务,整合所有底层能力,快速迭代前台功能。
支撑域
支撑域是对核心域有所支持,但不是业务的核心竞争力的部分。这部分的业务规则相对简单,通常不需要深入理解业务需求,只需要满足基本的业务需求即可。
2.3 实体和值对象
什么是实体?在业务中有具有唯一标识的对象。比如在电商场景下,一个物品对象就可以是一个实体,物品有唯一标识符(物品 id),物品的业务表现可能会发生变化,但是标识符在整个业务周期中是保持一致,比如一个物品在购买前是商品、购买后就变成了需发货的货物、如果要起退款就变成了一个需要召回的物品,但始终物品的标识符不会改变。
什么是值对象?针对一个实体对象,光有一个唯一标识是不够的,它不足于描述对象的特性,所以就有了属性,比如一个商品的属性一般有名称、价格、图片、生产地,而值对象就是一个业务实体属性的集合。
在实践中,业务实体往往对应着一个实体类,这个实体类有唯一的标识、属性、以及其所有的业务方法。领域驱动提倡使用充血模型的方式,即在类中实现所有相关的业务方法,而不是只把数据直接对外暴漏,这样可以很好的保证了业务数据的一致性和封装的特性,以下是一个物品对象的类实现。
//实体 //物品类public class Product{ private String productId; //唯一主键 唯一标识 private String productName; private String productUrl; private String productPrice; Private Address productAddress;// 属性集合 // get set 业务行为 ... public function{}}//值对象 //仓库地址类 (无主键id)public class ProductAddress{ private String Province; private String City; private String District;}
2.4 聚合和聚合根
当我们需要完成一个业务功能时,往往不是一个人就可以完成,而是大家协同工作,一起完成目标,在领域驱动中,实体就好像我们每一个人,聚合就是可以让我们协同工作的组织,聚合根就是这个组织的领导者,所以聚合其实就是由业务逻辑紧密关联的实体和值对象的集合,每个聚合有唯一的聚合根和业务边界,聚合一般会根据业务单一职责和高内聚原则设计,来确定其需要包括哪些业务实体以及值对象。
我们以购物车场景为例来体会一下聚合和聚合根的含义,在购物车中,加入购物车的商品列表构成了一个聚合,购物车 id 即聚合根,通过购物车 id,外界可以访问购买物品的列表信息、状态、下单总金额等。
如果要用代码实现这个简单的场景,我们很自然地想到可以把购物车的相关逻辑实现在一个微服务里,实际上,在领域驱动中,一组相关的业务聚合往往通过一个微服务来实现。再初步了解领域驱动的相关概念后,我们梳理一下它们之间的关系,如图所示。
03
拆分与合并
上一节我们已经讲解了一些领域驱动中一些重要的概念,这一节我们会介绍领域驱动中,关于分治与合并思想的落地,下面我们就分别讨论这两个过程 。
3.1 拆分与微服务架构
我们还是回到归并排序的案例中,思考一下平时所写代码与归并排序的相似之处,我们简单的对归并排序代码做一些改造,如下:
void mergeSort(std::vector
想想看,这不就是我们平常写的应用层代码吗,先远程同步调用 A 服务获取信息,再同步调用 B 服务,再组合所有结果数据进行运算并返回,如下图所示。
这不就是微服务的分层架构嘛!领域驱动最后的落地实现形式就是微服务,到这里就可以回答上面提出的一个问题,为什么领域驱动的理念是02年提出了,但是到了15年后才被人熟知,就是因为云原生、微服务架构的发展是在15年左右,这给领域驱动的理念提供了一片可以生存的土壤;相反领域驱动也给微服务的设计提供了必要的方法论。
清楚微服务架构的同学一定知道:微服务架构设计的难点之一是拆分服务的力度大小,拆多了会导致运维难度呈指数提高,拆少了又回到了单体架构的模式、不够灵活,中间这个度就需要一种方法论来指导,而这个方法论就可以是领域驱动。对比领域分析模型和微服务架构,你会发现其实都是相互对应的,只是一种是从业务角度出发描述问题,一种是从技术实现角度描述问题,而这也是理论和实践的一种结合。
3.2 合并的最佳实践:领域事件
在描述业务的过程中,往往会有这样一种描述:当某种事件发生后,会触发后续的事件或者用户进一步的行为操作,在领域驱动中会把这种有明显先后因果逻辑的事件称之为领域事件。
在领域事件中,会发现不同事件往往属于不同的领域服务之间,比如用户在购买物品支付成功后,会触发发货流程,这里的支付和发货就属于不同的领域,并在逻辑上有先后的顺序。
针对以上事件,领域驱动提倡:领域事件的数据通信方式使用事件发布订阅的方式进行,不直接同步调用,而事件发布的本质则是一种低耦合的异步数据沟通方式。
同步调用有两点需要考虑的问题:分布式事务以及时耗。针对事务问题一般需要引入第三方组件或者在业务层处理各种超时失败等异常的场景,可以说相当的复杂,维护成本也很高;而在分布式系统中时耗问题会被放大,一个请求可能跨越十几个甚至几十个服务,高并发的场景下,超时的风险会增大,上游的接口可能会被拖死。这都是同步调用需要考虑的问题。
在领域事件这种场景下,有一个更好技术选择,则是使用事件发布订阅的方式,还是拿用户购买物品支付发货场景为例,看看其实现过程:
用户支付下单后,支付域创建事件,持久化事件状态,在支付成功后发布事件,支付行为结束。
发货域订阅支付事件,在收到用户支付成功事件后,触发用户所购买物品的发货,持久化事件状态并结束。
用户收到发货成功通知,等待收货。
领域事件的本质其实是通过分析用户旅程找到领域之间的因果逻辑链,再通过事件发布订阅机制去实现流程上的解耦合。
那我们如何找到领域事件呢?一种最佳的实践是在领域专家的主导下项目相关的同学一起进行头脑风暴,联想和关联到和业务有关的所有事件,但是这里的难点并不是如何发散,而是发散后如何收敛事件,收敛的本质是对于事件的有效分类,这需要可以洞悉业务本质的人才可以做到,所以这就是为什么领域驱动中有一种角色叫领域专家 ,这个过程我也用图来表示。
04
总结
以上就是我对于领域驱动的一些浅见,如果你看完后还是感觉领域驱动有点形而上学,没关系,只要你记住,不管是技术还是生活,遇到事情多沟通,复杂问题先分解。
作者:吕昊俣