DDD,中文名为领域驱动设计,为业务开发中必不可少的指导方法论,本文以业务开发中战略设计和战术设计为例,将普通开发模式和 DDD 模式进行对比,让大家对 DDD 有个整体的认识。
我们在工作中,可能常常听到过下面两种声音:
产品说:这个做出来的东西,不是我想要的,体现不了业务价值。
开发说:你要做的东西和目前系统设计冲突了,如果要做的话,工作量比较大,改动点很多!(感觉在乱提需求……)
产生这两种声音的原因有很多,有的人说是沟通问题,有的人说系统设计不够扩展,而我觉得这些问题都只是表象,不是问题的根源。
DDD 完全可以解决以上两个问题,在战略设计阶段可以解决业务价值的问题,在战术阶段可以解决系统和业务需求不一致的问题。接下来我们按照常见开发流程,把普通开发模式和 DDD 开发模式对比一下,看看两者有啥区别,大家也看看这两个问题具体是怎么被解决的。
业务开发的几个大阶段如下:
( 公众号:架构精进 )
接着我们按照几个大阶段分别来描述下 DDD 模式有什么不同。
需求评审阶段
我们先描述一下需求评审的场景:
产品经理把开发、测试等相关的人预约到一个会议室,产品把写好的需求文档拿出来讲,大家听,不懂的就问……
不知道其他的需求评审会是不是这样的,反正我经历的基本是这样。不是说这种不对,只是感觉这样似乎缺少一些重点?
需求的来源?
需求评审上如何提问?
需求评审的结果是什么?
需求的来源
常规评审会的做法上,需求的来源都在需求文档上,需求文档写了什么需求,我就设计什么样的系统,需求文档中需求基本都是为了完成特定的场景,不是很全面完整,可能会导致系统设计的不够扩展。
抛开技术不谈,系统设计难以扩展的很大原因就在于需求的不完整。设想一个很大的需求场景,你拿到的只是部分,你设计出来的系统能够满足后来的扩展么?
DDD 强调的有:
需求不要仅仅局限于需求文档,要多多联想生活,个人工作经验,多想想竞品的实现。
不确定的细节多和领域专家讨论讨论,注意这里领域专家不一定是产品经理,领域专家指的是对业务的广度和深度理解比较强的人。
这两点非常重要,如果你真的实施下来了,最后的结果可能是你得到了 10 个场景,但需求文章只做其中的 2 个场景,但剩下的 8 个场景没有浪费,可以为你的系统设计做好扩展,因为你清楚,以后这里可能会改。
至于如何扩展,核心在于抽象,你可以把共用的事情抽象,可以把行为抽象,可以把流程抽象等。
需求评审上如何提问?
需求评审会的主要目的是为了弄懂业务和需求,但真正的弄懂业务和需求是什么,太难了,有可能你专注于这个业务几年后,才能真正知道该如何定义这个业务,所以我们该把这个目标缩小一下。
于是一般的评审会,大家就去听听流程,听听我们要做成什么样子。
DDD 更加强调业务的本质,最后达到能用一两句话直白地把业务描述出来的效果,为了达到这种效果,我们常常会有一些方法,比如:
抓住动词,联想名词
WR 原则
抓住动词,联想名词
我们小时候学习语文的句子,经常会说到主谓宾,然后为了锻炼我们的语感和对句子的理解,记得那时候还有组句和完形填空的题目。同理,业务需求其实也是一个句子,为了理解这个句子,我们完全可以用小时候做题的方式,但为了方便,我们简化成:抓住动词,联想名词。
我们举个常见场景:
小美在某宝 App 买东西。
这句话比较简单地描述了买东西的场景,我们用抓住动词联想名词来描述一下这个场景,可以简单描述成这样:
( 公众号:架构精进 )
通过这种方式,我们可以把业务描述抽象成概念,并且把这些概念丰富。比如说买的东西,我们抽象成商品,并把商品丰富成实物商品、虚拟商品、海淘商品等。
WR 原则
九十年代的时候,有个外国人发表了 5W1H 分析法,后人把这种方法称为思考问题的基本方法。我们在 DDD 落地时尝试过这种方法,发现很有用。5W1H 分析法有一张图很清楚的,图片来自于百度百科:
( 公众号:架构精进 )
这个图表达了对问题的一种思考套路。
图纵轴就是分析法中的 5W1H:What、Why、Where、When、Who、How,具体含义图解中已经说的很明白了。
图横轴,是对 5W1H 的每个阶段的深入解析,帮助我们对当前阶段有更深的理解。分别是四个问题:现状如何、为什么现状如此、能否改善、该怎么改善。
图纵轴和横轴乘一下,是 24 个方格,是对问题的思考结果,整个思考下来,我们对一件事情应该会有一定的认识了。
在分析业务时,5W1H 分析法会帮助我们理清业务的流程,但我们得不到一个结果,在业务开发过程中,结果往往是非常重要的。我们还是举买东西的例子:
买东西的例子我们抽象成了下单场景。在领域建模的时候,我们往往会用订单这个名词,来承接下单的所有因果,这个订单就是本次下单的结果,记录着下单的所有信息和后续履约过程。如此的例子有很多,比如下单之后,商家去发货,会有发货单,买家来退款,会有退款单等,我们建模的时候,都会用一个结果来承接所有的动作。
所以我们在 5W1H 的基础上加了一个名词,叫做结果,英文为 Result,总体简称 WR 原则。
5W1H 强调的是分析的过程,但我们领域建模使用更多的却是分析的结果。
所以在需求评审的时候,针对每个需求,你可以从抓住动词联想名词 + WR 原则出发,来进行提问,比如问:我们为什么需要做这个业务?价值在哪儿?
需求评审的结果是什么?
一般的需求评审会的结尾都是,产品经理开始问:
大家有木有不懂的,不懂的提问。大概几秒的沉默,产品经理接着说,那好,需求评审结束了,说白了,本次需求评审的产出,其实就是那么一篇需求文档。
我们实践 DDD 的时候略有不同,我们要求需要评审会结束后,最好有个最初版的通用语言出来。
我们对通用语言的定义:一定上下文内,对业务概念的一致通用表达,是理清业务是什么,能干什么,以及和其他业务边界的过程。
但在实际的工作中,需求评审会上产出初版通用语言太难了,你很难要求产品经理在需求文档中写出重要的通用语言,不太现实,那么只能开发自己来了,我们会要求懂 DDD 的同学在需求评审会结束时,发出来通用语言的 wiki 地址,要求主设计人员,在技术评审前,通过各种手段,完善好通用语言,并记录在文档中。最近我个人在落地一个项目,总结出来的通用语言,给大家展示下:
( 公众号:架构精进 )
技术评审
在需求评审后和技术评审前这个时间范围内,主导人员主要是开发人员了,开发人员需要在技术文章中,产出三个重要部分:
通用语言
领域模型
数据模型
通用语言我们上图也展示了一个,表达的形式没有标准,表格,图形都行,一般好的通用语言有如下几个特征:
描述比较直白简单,基本都是一句话,最好让小白都能看懂。不会用一个很难理解的名词去解释另外一个名词,也不会很复杂啰嗦。
重点通用语言会有英文描述。
通用语言有顺序,会循序渐进。
领域模型
这里其实是 DDD 落地最复杂的地方,也是耗时比较久的地方,我们最好能在业务评审的时候,就要开始去关注领域模型。
很多人对领域模型不是很了解,不知道领域模型到底是啥,通俗的来说:领域模型就是用来定义领域元素,和管理领域元素的上下文的。
那么又会有人问,领域元素有哪些,领域元素的上下文又是啥?
我们常见的领域元素有:实体、值对象、聚合、领域服务、应用服务、领域能力等。
领域元素之间的上下文指:元素间包含关系和逻辑关系。
我们贴出来一张完整的领域模型图(不包含和外域的上下文),感兴趣的同学可以自己研究研究:
( 公众号:架构精进 )
数据模型
数据模型,直白来说,就是在领域模型的基础上进行建表,我们需要表达出一种存储结构,这里特别推荐一本书,叫做《彩色 UML 建模》 ,这本书目前已经不印刷了,网上只有高价买原本的才有,这本书是我看过建模书籍中说的最好的一本了。
还有一些另外的建模小技巧,都是很常见的建模手段:
表的二级结构,很多场景下表都可以设计成二级结构,如总账和明细,订单和商品等。
不要害怕字段冗余,很多时候冗余是件好事,可以帮忙我们减少表的关联,增加查询的速度,有时候完全按照三范式建表可能会增加很多成本。
大宽表,像 ES、Hive 这种非关系型数据库,我们常常会建大宽表,来方便搜索。
虽然说我们在实践 DDD 的时候,尽量不要让数据模型来影响了业务,但有些时候,还是要结合一下公司的规模和成本作一些让步,争取尽快落地。
当通用语言、领域模型、数据模型都准备好了之后,当然我们这里准备的只是初版,我们在技术评审会上,需要把团队成员都叫上,由开发主导,向各个成员展示我们的初版成果,大家可以一起讨论,有争议的地方,讨论出结果后,再修改掉。
技术评审算是全员参与的第二个会议了。
测试评审
测试评审会的主导者是测试工程师,开发和产品需要注意的有:
测试工程师的表达是否符合通用语言,不符合请纠正。
测试工程师的理解是否和你理解一致,不一致请讨论。
以上阶段完成,DDD 战略设计也基本完成了,其实和普通的设计流程差别一致的,但思想侧重点不一样,DDD 更加侧重于如何想清楚业务是什么、能干什么、边界在那里,战略设计的所有产出都是围绕着这三个问题展开的。
战术设计
战术设计直白来说,就是写代码了,我们在使用 DDD 模式来写代码时,我们对代码有着严格的约束,大的来说模块、package、类的分层约束、调用约束等,小的来说模块,package,领域元素都是有命名约束,通过这样约束,让代码展示出来,就像展示出业务一样,即代码即设计、即业务。
战术方面内容太多,没有几十篇说不完,我们就说一点:如何写出高内聚低耦合的代码。
我们通过 MVC 和 DDD 两种架构,选取下面两个方面简单阐述下:
分层
领域层富血模型
分层
大家搭建项目的时候,都会通过模块来进行分层,大家讨论最多的是分三层好,还是分四层好呢?很少见有人去思考分层的好处所在。
分层只是达到系统高内聚低耦合的一种手段。
高内聚:指的是对领域层的内聚。
低耦合:指的是领域层对上下游的耦合少。
所以我们以下内容,并不是比较分三层还是四层还是五层的好坏,我们主要是来看看那种分层结构比较容易让业务达到高内聚低耦合。
我们来看下传统的 MVC 三层结构是否可以实现高内聚低耦合,画一张图描述下 MVC 三层结构:
( 公众号:架构精进 )
MVC 三层指的是 Controller 控制层、Service 业务逻辑层、 Model 数据层。
从图中的三层结构来看,其实是可以做到高内聚低耦合的,但很难。
接下来我们从两个角度来分析一下:
domain 层和上下游的关系
domain 层自己内部的关系
domain 和上层间的高内聚低耦合
Service 上游是 Controller 层,Service 层定义接口,Controller 层进行调用,这点是做到了依赖抽象。但在实践中,2 个很大细节被忽略,导致了违背了高内聚低耦合的原则。
细节一:DTO 流入了 Service 层
大家应该是知道,DTO 是对外的数据载体,是当前业务场景的输入和输出,即是应用服务的输入和输出。在实际对接的过程中,场景的输入和输出是很难制定标准的,很多时候需要去适配别人,所以我们希望 DTO 的身影只出现在 Controller 层,但实际工作中,DTO 也会经过 Controller 层流转到 Service 层,这就导致上游的业务已经侵入到本域了,违背了低耦合。
细节二:Controller 层写业务
Controller 层有个原则,我们只做流程编排、参数转化、事务等事情,绝不写业务,但实际的工作中,这个原则也经常被忽略,我们常常看见 Controller 层被大量的业务逻辑充斥着,这就导致了业务逻辑从 Service 层转移到了 Controller 层,导致业务逻辑分散在两个模块上,违背了业务内聚的原则。
那么这种情况在 DDD 中是如何改善的呢,我看过网上一些同学写的 DDD 代码,即使使用 DDD,只是把两层换了名字,换成了 app 层和 domain 层,但这种问题仍然没有解决,特别是在业务特别复杂的时候,问题还是存在,于是我一直在想,究竟是哪里做的不对?
这个问题的根源在于 Controller 层和 Service 层的边界没有划分清楚(这里说的边界是技术边界,业务边界很多同学都划分的清楚,但写代码的时候,代码完全体现不出来业务边界),那么可能同学会说,哈哈,其实我也知道 Controller 层很薄,没有业务逻辑,业务逻辑都在 Service 层,但团队越来越大,业务越来越复杂,这个边界也随着划分不清楚了,慢慢的失控了?
是的,会失控的,但也有办法可控,我们在实践中通过以下两种办法可控:
代码自动生成,生成的业务代码已经规定好了业务约束,开发人员不会乱写。
app 层和 domain 层的边界定义了约束,app 层只能够调用 domain 层的实体行为、聚合行为和领域服务三种领域元素,其他领域元素都不允许出现在 app 层。
自动生成业务代码保证了项目的整体技术框架,可以按照设定的走,不会随着团队规模的扩大而乱掉。
app 层只能调用 domain 层三种接口,也是通过代码自动生成来控制的。
通过这样子的技术约束来固定了代码架构,从而体现业务边界。
有的同学可能会问,这样子做,技术会不会去影响业务?肯定是不会的,我们是先设计领域模型,然后用对应的代码去实现领域模型,这样的技术只会让业务在代码中得到最大的体现。
只要你知道领域模型和代码是如何一一对应的,你会发现,看代码就像看业务文档一样。
DDD 能做到这一点,主要是因为 DDD 将领域层进行了细分,比如说领域对象有实体、聚合,动作和操作叫做领域服务,能力叫做领域能力等,而 MVC 架构并没有对业务元素进行细分,所有的业务都是 Service,从而导致 Controller 层和 Service 层很难定义出技术约束,因为都是 Service,你不会知道这个 Service 是用来描述对象的?还是来描述一个业务操作的?
DDD 将领域层进行了细分,是我个人觉得 DDD 比较 MVC 框架的最大亮点。
我认为能做到以上 2 点的 DDD 业务框架才会比 MVC 好些,否则的话,在 app 层和 domain 层之间,两者并没有差别!
domain 和下层间的高内聚低耦合
在 MVC 中,Service 层的下层是 Model 层,Model 层是数据层,我们可以简单理解成 Mysql、es、Redis 等等,通常由 Model 层定义一个接口,Service 层去调用,那么这时候问题来了,Model 层的改动会不会影响 Service 层?
会的,肯定会的,Model 层的改动肯定会影响 Service 层!但这还仅仅是技术上耦合的地方,并不是致命点,致命点是这种依赖会导致业务的耦合!
假设现在 Model 层由 A 来维护,Service 层由 B 来维护,那么 Model 层的接口将由 A 来定义,A 定义出来的接口,应该是按照 Model 层的标准来的,然后 Service 层会去调用 A 的接口,那么问题来了:A 的接口是 Service 层想要的么?符合 Service 层的业务发展需要么?
一个两个可能是,但如果是一百个呢?答案肯定是否定的,这时候 Service 层只能去适配 Model 层的接口,是不是很奇怪,核心业务居然要去适配底层数据的储存结构?这样做的系统,就是大家都说的数据模型驱动的业务系统,是以数据模型出发生产出来的业务,而不是以实际业务出发生产出来的业务,这句话有点点拗口。
为了解决这个问题,DDD 提出了非常棒的解决方案:依赖调用!
MVC 是 Service 层依赖 Model 层,DDD 却完全相反,domain 层的下游都需要去依赖 domain 层!
直白地说,就是 domain 层如果需要什么,就自己去定义接口,然后下游去实现,因为接口是自己定义的,所以业务是内聚在 domain 层,然后 domain 层也不会去耦合下游的业务。
有的人会说,这不就是一种依赖抽象么?我理解绝不是这么的简单,我理解这是前辈在 MVC 的痛点基础上,想出来的通过一种技术手段来解决了模块之间的边界问题,是架构间的慢慢演化,发现目前架构的痛点,并通过演化,提出一种新的架构思想,这是很了不起的,绝不是什么依赖抽象这样的大话。
这里面其实体现了一种思路:我们是可以通过一些技术约束来划清出领域的业务边界的。
前面所说的 app 层和 domain 层的两种解决方案,我也是借鉴了这种思路,技术不能去影响业务,但能够去反哺业务的。
代码就不上了,都是很简单的实现,大家可以去星球自动生成代码看看。
领域层的富血模型
分层说的是领域层和其他域之间的关系,用国家来形容的话,说的就是外交,接着我们要来说下自制,领域层内部各个领域元素之间有什么关系,又该如何管理呢?
在此之前,说下 DDD 中三个概念,贫血模型、富血模型、充血模型,三个概念都是用来描述对象状态的。比如说这个实体很贫血,这个聚合充血,而我们的目标是富血,直白来说,贫血就是该做的事情没做,没有尽职;充血就是做的事情太多,把不该做的事情也做了,管的太多;富血就是做的事情刚刚好,我只做了我该做的事情。
为了达到富血模型,我们认为两点是特别重要的:
领域元素的定义要清楚
领域元素的边界要清楚
DDD 中的领域元素其实还满多的,有实体、聚合、领域能力、值对象、领域服务、应用服务、工厂、仓储等。
要想搞清楚,可以先看看 DDD 经典书籍:《领域驱动设计:软件核心复杂性应对之道》,其他理论派的 DDD 书籍个人都不推荐,最最关键的是需要实战,我基本上也是在实战了公司 2~3 个项目之后,才慢慢对这些领域元素有较深的理解了。
领域元素的边界要清楚
边界主要有两个思考的出发点,包含关系和逻辑关系。
其实如果你真的对领域元素的定义理解透了,其实领域元素的边界也出来了。
比如说聚合是来管理实体间一种固定的业务关系的,聚合和实体是一对多的关系,聚合会包含多个实体。
比如说值对象是用来描述对象的,那么值对象只可能用来描述实体,而不是聚合。
比如说领域服务是一种操作或动作,你会发现领域服务在边界解耦,组装领域能力方面也有很大的作用。
领域元素的定义才是非常关键的,吃透了,领域元素的边界自然就清楚了。
篇幅有限,就到这里了,本文其实一点点细的东西都没有说,基本说的都是一种思路,让大家对 DDD 有个简单的认识。