架构三班 李大成
2020年7月14日 更新
开启更多功能,提升办公效能

分享人:李大成

分享主题:关于DDD的那些事儿

分享提纲:


自我简介

我在北京工作,是一名Java开发工程师(有架构师和项目Scrum master的角色,公司不大,组织扁平, 暂无明确的title)。


我同意李老师的观点, 架构师是一顶帽子,是一个角色,不是一个title,只要自己做的是架构设计相关的事情,自己就是一名架构师。所以我基本上以架构师自居~希望通过在这里的专业培训,成为真正专业靠谱的好架构师。


我们公司所在领域是销售管理的CRM-Saas软件,目前在此领域,我司在国内的诸多软件供应商中处于TOP-3的层次,所在的部门主要是给Saas业务侧提供基础的Paas平台,包括:元数据,权限,License,基础查询搜索,分布式任务调度等基础技术组件服务。


接下来,我开始给大家逐个介绍一下DDD的主要概念。


DDD的历史背景和基本理念

什么是DDD

DDD是Domain Drive Design的缩写,直译的意思是领域驱动设计的意思。

DDD的概念大约十五六年前由Eric Evans 提出 , 《领域驱动设计》一书可以算作是Eric在DDD领域的的开山之作。


中文版封面是这样的,各大电商网站都能够搜到购买链接:


请各位同学注意这本书的副标题: 软件核心复杂性应对之道。 大家还记得李老师给我们讲课的时候,反复强调的一个观点吗? 就是做架构师,一定要搞清楚要解决的问题是什么。采用某一个架构模式或者技术方案的前提,一定是要正确的识别当前遇到的问题是什么?你的方案,是否适合用来解决自己遇到的核心问题?


本书的副标题说的很清楚,领域驱动设计的目的,是为了解决软件的复杂性的。而这个复杂性的主要的着眼点,主要是业务层面的复杂性。


所以说, DDD主要是面向复杂业务的,比较适用的场景,一般都是业务逻辑极其复杂的场景(To B场景居多,例如ERP,CRM, 等等,当然,现在to C领域,例如电商也有很多适合DDD发挥作用的场景,现在的互联网已经进入深水区,除了对高并发高可用高性能的追求,业务的复杂性也是越来越高)


说这么多,就是希望同学们在打算进入DDD领域学习之前,想一想自己在工作中遇到的核心问题是不是业务复杂性带来的问题?带着问题来学习,才会有代入感,才能更好 的理解DDD中繁杂的概念和思想,并给你的工作带来一些好的解决问题的思路。


除了上面这本书以外,其他经典书籍:《实现领域驱动设计》《领域驱动设计精粹》等等

国内DDD社群:http://ddd-china.com/

国内社区内比较活跃的人物:Thoughtworks的 王威,肖然, 张逸(前thoughtworks员工)这些大佬们的PPT和近年来在DDD峰会上的演讲视频录制材料在ddd-china网站上能够搜索得到。

如果大家感兴趣,也可以考虑报名下一次的DDD峰会, thoughworks组织的还是很不错的。

19年录播链接:https://www.itdks.com/Act/apply?id=3188&from=search

18年录播链接:http://www.ddd-china.com/look-back-2018.html



另外,在极客时间上也有适合DDD入门的技术专栏:欧创新 的 DDD实战课 https://time.geekbang.org/column/intro/23


适合进阶深入学习的技术专栏:张逸 的 领域驱动设计实践(战略+战术)https://gitbook.cn/gitchat/column/5cdab7fb34b6ed1398fd8de7


战略设计

DDD的理念,从高层次的角度来看,主要分两大块:战略设计和战术设计。

我觉得可以把这两个大的概念映射到传统软件设计上:

战略设计 对应着 概要设计

战术设计 对应着 详细设计

统一语言

统一语言体现在两个方面

【名词】:统一的领域术语。尽量基于业内通用标准。而且要尽量给出英语的统一术语。以便于指导代码的类和包命名。


【动词】:领域行为描述。需要满足以下要求:

从领域的角度而非实现角度描述领域行为

若涉及到领域术语,必须遵循术语表的规范

强调动词的精确性,符合业务动作在该领域的合理性

要突出与领域行为有关的领域概念


统一语言是团队中不同角色之间高效沟通协作的大前提


人类的语言的不完美的,往往都是有强烈的二义性的。二义性会带来很多笑话,也会带来一些麻烦:

例如在英文里 look out 往往是”危险“的意思, 但是表面上看look out却是”向外看“。如果你正在开车,你身边的金发美女老外跟你喊look out,你就真的扭头往外看,而不是小心的握紧方向盘开车,那就真的很危险了。


还有一个有点意思的笑话:

几位中国同学邀请刚来华学汉语的外国男生吃饭。其间,一人说出去“方便一下”,外国学生不懂其意,于是大家告知方便就是上厕所,这名男生记住了。


此后不久,一名中国女生提出,希望在他方便的时候拜访他,这位男生愕然并立即摆手:“你什么时候来都可以,但就是在我方便的时候不能来。”


对于软件世界来说,任何定义上的二义性往往都会带来很大的麻烦。如果没有统一的语言,整个组织的沟通协作效率会大大降低。



举几个我们公司的例子(最开始我们在统一语言方面做的不是很好,有时候的沟通就会出问题):

  1. 鸡同鸭讲。(举例子:一个开发和新来没几天的产品经理说,需要提供对象Describe,还需要知道对象之间是lookup还是MD。产品慌了:“啥是describe?啥是lookup?啥是md?”)
  1. 歧义或者二义性。(举例子:企业初始化(新租户注册) vs 企业初始化(老租户一键恢复出厂)。



各种域的概念


咋一看上面这个图,大家有什么样的感觉呢? 感觉这个图像是什么呢?


我最开始看到这个图的时候,想到的就是构成生命体的最重要的一种物质或者结构: 细胞体。

我认为软件的架构方式,就应该像生命体一样,有清晰的“细胞体”“细胞壁”“细胞核”等等元素,才称得上是有生命力的的软件吧。


接下来解释一下这个图:


【领域】Domain:领域就是用来确定范围的,范围即边界,这也是 DDD 在设计中不断强调边界的原因。简言之,DDD 的领域就是这个边界内要解决的业务问题域。 边界是非常重要的。

例如 :一个CRM系统,可以是一个领域,一个HR系统,一个电子商务的商城,都可以作为领域概念。 如果一个软件公司,既提供CRM的Saas服务也提供e-HR的Saas服务,但是这两种业务是有很明确的边界的,那么这两个业务就要各自独立为两个不同的领域。不能因为是同一家公司的产品,就混在一个领域范围内。


【子域】Sub domain:领域可以进一步划分为子领域。我们把划分出来的多个子领域称为子域,每个子域对应一个更小的问题域或更小的业务范围。 以一个CRM系统来举例:权限和登陆是个子域, 销售自动化是个子域,BI和统计分析模块是个子域等等。


【核心域】Core domain:不同的软件的核心业务是不一样的,这里的”核心“一般指的是业务角度。例如对于电商网站(例如淘宝和京东)来说,核心业务一般都是购物车和下单的交易和支付领域。而权限领域或者社交通讯领域的业务重要性就是其次的。


【通用域】Generic domain :一般来说,被多个子域所依赖的子域就是通用域。例如登陆和权限。对于初创企业,为了快速上线和节省成本,是可以考虑外购部分通用域的。


【支撑域】Supporting subdomain: 一般是只不是系统中的最核心模块,但是也不是通用的组件和服务,但是对核心业务起到了支撑的作用的模块。


为什么要区分这么多不同的域呢? 我觉得 欧创新在《DDD实战课》里面的解释和举例是非常通俗易懂的:

拿桃树来说吧。我们将桃树细分为了根、茎、叶、花、果实和种子等六个子域,那桃树是否有核心域?有的话,到底哪个是核心域呢?

不同的人对桃树的理解是不同的。如果这棵桃树生长在公园里,在园丁的眼里,他喜欢的是“人面桃花相映红”的阳春三月,这时花就是桃树的核心域。但如果这棵桃树生长在果园里,对果农来说,他则是希望在丰收的季节收获硕果累累的桃子,这时果实就是桃树的核心域。在不同的场景下,不同的人对桃树核心域的理解是不同的,因此对桃树的处理方式也会不一样。园丁更关注桃树花期的营养,而果农则更关注桃树落果期的营养,有时为了保证果实的营养供给,还会裁剪掉疯长的茎和叶(通用域或支撑域)。


限界上下文Bounded Context

前面说过了”统一语言“的概念。 其实统一语言和限界上下文是有很深的联系的。 欧创新老师的DDD实战课也讲了一个很不错的例子:

在一个明媚的早晨,孩子起床问妈妈:“今天应该穿几件衣服呀?”妈妈回答:“能穿多少就穿多少!”那到底是穿多还是穿少呢?如果没有具体的语义环境,还真不太好理解。但是,如果你已经知道了这句话的语义环境,比如是寒冬腊月或者是炎炎夏日,那理解这句话的涵义就会很容易了。


所以语言是离不开语义环境的,任何的“统一语言”一定要建立在明确的“限界上下文”之中,才能是真正的统一语言。


在我们做系统设计的时候, 限界上下文的切分是非常重要的。这边Bounded的边界,要不大不小正合适。一般来说的原则如下图:

举个例子:

在CRM的销售自动化领域中, L2O 和 O2C 两个阶段就是典型的两个很自治的子域:


L2O一般从公海和线索开始,到生成客户和商机并生成了订单。 是一个售前的相对很完整闭环的流程。很适合划分为一个独立的限界上下文。


而O2C 是指从订单到回款和应收阶段,的完整闭环的售中和售后流程,也非常适合单独划分为独立的限界上下文。


上下文映射

那么什么是上下文映射呢? 上下文映射是来讨论限界上下文之间的协作问题的。 两个不同的限界上下文之间是有关系的,而且这个关系是有方向的。

一般为两种关系: 上游(Upstream),下游(Downstream)。 在上下文映射图中,以U代表上游,D代表下游。如下图所示:



上面战略设计部分主要涉及到的概念就介绍完了。接下来进入战术设计。


战术设计


贫血模型和充血模型

开始介绍战术设计之前,先聊一下什么是贫血模型,什么是充血模型。


经典的 Java 三层架构对领域模型的设计。在这个三层架构中,领域逻辑被定义在业务逻辑层的 Service 对象中,至于反映了领域概念的领域对象则被定义为 Java Bean,这些 Java Bean 并没有包含任何领域逻辑,因此被放在了数据访问层。


这些 Java Bean 由于仅包含了访问私有字段的 get 和 set 方法,违背了面向对象设计原则的“对数据与行为进行封装”。”Martin Fowler 则将这种没有任何业务行为的对象称之为“贫血对象”。基于这样的贫血对象进行领域建模,得到的模型则被称之为“贫血模型”。


反之,如果对象的设计包含了属性和自身的行为的话,得到的模型则成为“充血模型”


为什么要聊着两种模型呢?目的是引出下面两个在DDD中非常重要的概念,实体和值对象。


实体与值对象

首先, 实体和值对象都是领域模型中的领域对象。 但是它们一定不是仅仅是普普通通的JavaBean或者POJO类就可以成为实体或者值对象。


上一节提到的贫血模型,就是在DDD概念中要尽量避免构建出来的领域对象。只有把对象的属性和行为都做好抽象和封装,才能发挥出面向对象设计和编程的强大威力。


那么到底啥是实体,啥是值对象?它们之间有什么区别呢? 这个地方在DDD里面是比较容易混淆的两个概念:

实体:一个典型的实体具有三个要素

身份标识,属性, 领域行为

身份标识类似于主键ID一样,是这个实体对象的唯一标识。(注意,包括 UUID 在内的随机数并不能支持分布式环境的唯一性,它需要特殊的算法,例如采用 SnowFlake 算法来避免在分布式系统内产生身份标识的碰撞。)


属性,就是对象上的property, 如果通过ORM映射到数据库,就是表上的一个个的字段。 这个跟典型的三层架构中的DAO层的JavaBean的属性概念是基本类似的。


领域行为,也就是实体对象上的方法。代表这个业务对象的各种业务操作。上面说的贫血对象和充血对象的核心差异点就在是否对象自身拥有领域行为上。


下面介绍一下值对象。是否拥有唯一的身份标识才是实体与值对象的根本区别。也就是说,值对象一般是一个具有不变性的无状态的对象。

例如,如下 Money 值对象的定义就保证了它的不变性,如下代码:


一般来说,值对象是需要依附于某一个实体来存在的。

在我们的公司的实践中, 我们管依附于实体的值对象,称之为“内嵌对象”。我们的实现方式类似于下例所示(摘抄与欧创新的DDD专栏):


聚合和聚合根

首先说一下聚合。简单的说聚合就是由业务和逻辑紧密关联的实体和值对象组合而成的,聚合是数据修改和持久化的基本单元,每一个聚合对应一个仓储,实现数据的持久化。


而聚合根呢,就是这一个聚合中的N个实体中的最核心的那一个实体。举个简单的例子。 有一个客户的聚合,其中包括了客户实体,线索实体,公海实体,地址值对象,账号值对象。而这个聚合中,最核心的那个实体是客户实体,那么客户就是这个聚合的聚合根。



领域服务

对于一些复杂的业务,需要跨多个实体和值对象来进行业务操作,这时候,如果把相关操作封装在某个单一的实体中,是不合适的。这时候需要引入领域服务的概念。


领域服务会对多个实体和实体方法进行组合和编排,供应用服务调用。如果它需要暴露给用户接口层,领域服务就需要封装成应用服务。



资源库 Repository

资源库(Repository)是对数据访问的一种业务抽象,使其具有业务意义。利用资源库抽象,就可以解耦领域层与外部资源,使领域层变得更为纯粹,能够脱离外部资源而单独存在。

资源库的概念跟之前经典的三层结构的DAO层有类似之处。不过资源库的概念更加的抽象,如下图:


资源库的理念,是一种典型的依赖倒置,如下图,大家看着应该会觉得非常的眼熟,前面的课程中,李老师对这个概念也有过深入的讲解:

领域服务依赖的是一个资源库的抽象,而不是依赖具体的实现。这样就把领域层和具体的存储实现做了解耦,实现了依赖的倒置。


CQRS


CQRS的全称是 Command Query Responsibility Segregation. 指的是命令查询职责分离模式 Martin Fowler 大神在2011年对这个概念的这篇博文,引起了行业内对其关注,并逐渐融合到了DDD的理念之中: https://martinfowler.com/bliki/CQRS.html


CQRS的核心理念就是把查询服务和命令处理做分拆,和DB的读写分离有异曲同工之妙。


那么在应用程序层面做读写的分离,有什么好处呢? 首先我们分析一下查询操作和命令处理操作有什么区别:

  • 查询操作没有副作用,具有幂等性;命令操作会修改状态,其中新增操作若不加约束则不具有幂等性
  • 查询操作发起同步请求,需要实时返回查询结果,因而往往是阻塞式的 Request/Response 操作;命令操作可以发起异步请求,甚至可以不用返回结果,即采用非阻塞式的 Fire-and-Forget 操作
  • 查询结果往往需要面向 UI 表示层,命令操作只是引起状态的变更,无需呈现操作结果
  • 查询操作的频率要远远高于命令操作,而领域复杂性却又要低于命令操作

既然命令操作与查询操作存在如此多的差异,采用一致的设计方案就无法更好地应对不同的客户端请求。


一个典型的CQRS的架构图是这样的:


然而,使用CQRS之前,也一定要搞清楚是否适用当前的场景。不要乱用。一般来说,普通的比较简单的业务场景下,乱用CQRS反而徒增系统的复杂度,得不偿失。 如果命令和查询重叠较多,共用一个CRUD的模型反而更简单些。



领域事件

领域事件是指,发生后通常会导致进一步的业务操作的事件。举个例子:在XXX电商平台下单购买商品支付成功后, 支付成功就是一个领域事件。后续会导致进一步的例如减库存,发货,开票等一系列业务操作。


领域事件的技术实现方案,是很典型的适用适用各种MQ之类的消息中间件的场景。领域事件一般都是通过异步的消息来描述。


上面大体上就把战略设计和战术设计中的最主要的概念都介绍完了。 接下来介绍一个称之为事件风暴Event Storming的概念。


我没有把事件风暴归类到战略或者战术中,因为我认为这个概念相对来说对战术和战略的设计都有一定的指导意义。


事件风暴

其实在最开始的DDD的概念中,是没有事件风暴Event Storming这个概念的。这个概念是后来由Alberto提出来 ,详见: https://www.eventstorming.com/。 id,让不同角色的人,在一起把业务场景做好梳理,并把要构建的系统中的核心概念,流程,动作,事件都梳理的清清楚楚。

一般事件风暴的做法是这样:

1,业务人员,领域专家,技术人员,架构师,测试人员都要参与。

2,多种颜色的即时贴。

3,在一个开放的空间,最好有一面有很大的白板的很宽的墙的面前。以便于大家的互相讨论并把各种重要的概念通过即时贴体现在墙上。 thoughtworks的人也把这个过程戏称为“糊墙”。


给大家看一次比较成功的事件风暴后的成果物:


一般来说,经过一次或者几次的事件风暴后, 整个系统的战略设计的架构也就呼之欲出了。而战术设计层面的各种实体和值对象,聚合和聚合根以及领域事件也就基本都被识别出来了。


更多信息见:https://github.com/mariuszgil/awesome-eventstorming

DDD和其他概念的关联

DDD和微服务

DDD和微服务能够很好的结合在一起。 因为DDD的限界上下文的概念,能够很好的指导微服务的拆分粒度。 一般来说,一个限界上下文是适合作为一个微服务来独立部署的。 这样就避免了微服务架构下,服务拆分的过粗或者过细的问题。


DDD和敏捷

敏捷宣言中强调了沟通协作的重要性。著名的敏捷框架SCRUM中,一个闭环的SCRUM团队中,业务人员,开发人员,QA也都是团队中不可或缺的成员。这和DDD宣扬的开发团队和领域专家精诚合作,一起做业务的分析(事件风暴),是不谋而合的。而且敏捷的目的一般都是为了积极的相应变化和拥抱变化。 DDD的理念能够把复杂的系统抽象出更合理的架构模式,自然就可以更加方便的相应敏捷的需求。如下图(来自张逸的专栏),可以深刻的感受到DDD和敏捷迭代的融合:


DDD和中台

中台的概念,大火于2018年。 但是当时大家大多是讨论这个概念, 说的都是WHAT和WHY。到了2019年后,中台理念趋于务实和成熟。 这时候大家都在思考的就是HOW。而DDD的理念能够让中台概念有效的在企业内落地。中台建设是一定要聚焦在领域模型上的。中台不是目的,而是手段。 业务才是目的。业务领域模型梳理的足够清晰,才能够有的放矢的把中台建设好,并发挥出应有的作用。


今天我要分享的主要内容就这么多。我会把这次分享的内容更新到我的infoq写作平台上: https://www.infoq.cn/profile/1122666/publish ,欢迎关注和留言讨论。 再见!