我对 DDD 的一些理解和总结

发布于: 2020 年 07 月 14 日
我对DDD的一些理解和总结

前言

本文有50%内容是自己阅读过DDD的相关书籍和DDD的相关专栏后沉淀总结了一下自己的理解,另外50%内容摘抄自相关专栏和书籍,引用参考他人文章或者专栏内容之处,已经标明了来源。

另外本人已此文作为主要内容,在架构师训练营的社群分享中,做了文字直播。直播的过程,都是逐行拷贝本文内容发送到了微信群中的。

DDD的历史背景和基本理念

什么是DDD

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

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

 

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

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

 

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

 

所以说, 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?”)

  2. 歧义或者二义性。(举例子:企业初始化(新租户注册) 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)是对数据访问的一种业务抽象,使其具有业务意义。利用资源库抽象,就可以解耦领域层与外部资源,使领域层变得更为纯粹,能够脱离外部资源而单独存在。

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

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

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的理念能够让中台概念有效的在企业内落地。中台建设是一定要聚焦在领域模型上的。中台不是目的,而是手段。 业务才是目的。业务的领域模型梳理的足够清晰,才能够有的放矢的把中台建设好,并发挥出应有的作用。

后记

在后记里面,记录一下社群分享后,群内小伙伴们提出的问题&讨论等等内容信息,Q代表问题,A代表我的回答,如下:

Q:老师 ,刚才您提到敏捷,我问一个问题,敏捷如何运用到项目中(不是产品)

A:你关注的是开发过程,还是代码质量呢?如果是开发过程, Scrum框架是很好的跟进项目的方式。有好的过程,就会有好的结果。

Q:假如 现在敏捷都弄好了,然后ka一个紧急需求

A: 那就一定要有足够高的单元测试覆盖率或者自动化测试的覆盖率。这样,你的代码才能响应不断的变化,少出bug。脱离自动化测试覆盖率的敏捷,我认为都是假敏捷吧。关于敏捷,我还有一个理解哈, 敏捷是一种状态,是形容词,不是名词。 如果你能够快速的响应各种变化,才是真正的敏捷。 采用敏捷框架,只是名义上的敏捷罢了。 所以 你说你们的敏捷弄好了, 其实你们并没弄好。敏捷需要全公司不同的角色一起努力。 不光是开发的事儿。

Q:请问一下 聚合 和 资源库 有什么关系

A: 聚合是需要调用资源库做数据的存储的(自己应该是说反了)。资源库一般不涉及业务,就是对DB或者其他的存储,网络直接的资源的封装吧。聚合中,是有各种业务属性和行为的。这样就把业务逻辑,和具体的技术细节解耦开了

Q:做数据聚合的web服务适合用ddd 吗?

A: 数据聚合的web服务? 能举个具体的例子么?只是查询场景吗,这种适合刚刚说的CQRS模型

Q:接触ddd三年了,一直觉得很抽象,我觉得从战略设计上很好理解,但从战术设计上代码层面实施起来多多少少碰到些问题。ddd在java微服务有落地到生产的案例么?

A: https://github.com/ddd-by-examples

Q: DDD的领域服务直接依赖资源库有弊端吗?

A:我的理解是一般领域服务是解决跨多个聚合或者实体的复杂逻辑的,所以这样的业务操作不能封装在某个单独的实体上。这样的话,在领域服务里面直接依赖资源库应该是很难避免的吧。如果实在不希望直接依赖资源库,那就可以再抽象封装一层出来? 这个还真没试过。

Q: 应该是应用层来依赖资源库还是领域服务依赖资源库 我实践的时候总是很纠结。按照闭包原则,我不希望资源库的变更影响到领域层,我往往都会把repository的依赖放在应用层,但是总是有一些场景需要在领域服务耦合资源库,你们在实际工作中是怎么解决这个问题

A:一般资源库层面的变动是什么? 存储方案改变?还是升级?还是什么?

Q:表字段的变更

Q: 那应该是业务变化,不是资源库的变化吧.我理解的资源库的目的,是为了存储层面的解耦。 比如说现在用的是PG,将来可能有一天,要迁移到MySQL.这样的话,迁移的话,替换资源库就ok了

Q: 所以你会在聚合上依赖资源库吗

A:应该是资源库依赖聚合吧。资源库给聚合对象做持久化操作。 资源库调用聚合。聚合中不会有资源库。

Q:我实践中不这么去做,因为聚合对象有太多可改变聚合状态的行为。 聚合对象我只对领域服务开放

A:所以回到最初的问题, 你希望领域服务只依赖领域对象,不依赖资源库,对吧?

Q: 是呀。我想把资源库的调用都放到应用层。应用层负责接收客户端的请求,调用领域服务,调用资源库服务。因为复杂对象的创建 我又不希望放在应用层。

A:在我的认知里, 应用层,是更上层的。一般不会直接调用资源库。应用服务,一般理解成为一个外观facade服务。可能你们有你们的场景。些,都是自己实践层面的东西,没有绝对的对错。DDD是一种思想,不是一个死套的框架。

关于头脑风暴的讨论内容如下:

甲 : 国内公司确实很少做头脑风暴这件事。我觉得一是官僚社会里成长的我们总是会下意识的用阶级观念看问题,不习惯反对或质疑比自己高阶的人的观点。

二是业务方不愿意或者不屑参与细节讨论,也不太愿意接纳技术方提出的折衷意见。总是有类似“我的要求就是这样,我不管你用什么方法,就是要这样实现”腔调。

乙 : 头脑风暴的过程中,就是不允许反对和质疑的,

而是尽可能的收集方案,在过程中反对和质疑,会打击大家的积极性,从而无法形成风暴。另外,你说反了,国内公司经常在风暴过程中,上级或同级很容易去质疑别人的观点,

导致风暴中止。当然,上级发表了意见,其它人经常不太敢继续,从而也影响风暴的继续。

甲 :不允许反对意见,何来风暴?只有无限制的思想交锋才能让所有参与者都透彻理解别人的观点,并发现自己的想法的不足,最后才能汇总成一个更好的方案.不允许反对,每个人说一大堆自己的想法,然后呢?让领导做决定选一个?

乙:头脑风暴分几个阶段,发散过程就是尽可能收集,不反对,不质疑,天马行空都可以,

收集完,才进行讨论、分析、决策

甲 :只要是针对方案的质疑,管他谁提出的,都没问题。你强调领导的质疑会干扰会议进程,说明你就是在用阶级观点来看问题

我:一般来说正规的头脑风暴是这样, 会有发言权杖 。 拿着发言权杖的人表达观点的时候,不允许任何人打断,或者质疑,或者反对。头脑风暴第一个阶段的目的,就是收集所有人的想法。无论这个想法是否靠谱。

用户头像

LOVESKY

关注

从宏观的架构和微观的代码中洞悉软件的实相 2018.05.06 加入

之前有多年外企和大型传统IT企业的开发经验,近几年在2B互联网的Paas- Saas云服务领域持续摸爬滚打中。 爱好:读书,旅行,听音乐,跑步和一些球类运动,亲子育儿,历史,哲学心理学佛学等

评论

发布
暂无评论
我对DDD的一些理解和总结