干货分享:关于 DDD 的那些事儿

本文主要整理自架构师训练营李大成同学在社群的直播分享。根据其直播主题和相关内容,结合笔者自身理解以及以往实战经验,从 DDD 历史背景、基本理念、战略设计、战术设计、事件风暴和 DDD 指导应用,展开叙述。

一、DDD的历史背景和基本理念

DDD 即 Domain Drive Design 的缩写,中文直译是领域驱动设计。DDD 概念大概在十五六年前由 Eric Evans 提出 , 《领域驱动设计》一书可以算作是 Eric 在 DDD 领域的的开山之作。

DDD中文书
DDD中文书

DDD 中文版如上图示,请注意这本书的副标题 —— 软件核心复杂性应对之道。李智慧老师在架构师训练营讲课的时候,反复强调了一个观点, 就是做架构师,一定要搞清楚要解决的问题是什么。采用某一个架构模式或者技术方案的前提,是一定要正确的识别当前遇到的问题是什么。你的方案,是否适合用来解决自己遇到的核心问题。

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

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

除了上面这本书以外,其它经典书籍如《实现领域驱动设计》、《领域驱动设计精粹》等,国内 DDD 社群:http://ddd-china.com/,此网站干货也很多。

国内社区内比较活跃的人物:Thoughtworks 的王威、肖然、张逸(前 Thoughtworks 员工),这些大佬们的 PPT 和近年来在 DDD 峰会上的演讲视频录制材料,在 ddd-china 网站上能够搜索获取。如果大家感兴趣,可以考虑报名下一次的 DDD 峰会,  Thoughtworks 是一个很不错的组织。

参考资料:

  1. Thoughtworks 峰会19年录播链接:点击查看
  2. Thoughtworks 峰会18年录播链接:点击查看
  3. 极客时间-欧创新-DDD实战课 :点击查看
  4. 张逸 -领域驱动设计实践(战略+战术):点击查看

DDD的理念,从高层次的角度来看,主要分两大块:战略设计和战术设计。个人觉得可以把这两个大的概念映射到传统软件设计上:战略设计对应着概要设计,战术设计对应着详细设计。而战略设计,主要涉及如下几个概念。  

二、战略设计

1、统一语言

统一语言体现在两个方面。一方面是统一的领域术语,即尽量基于业内通用标准,而且要尽量给出英语的统一术语,以便于指导代码的类和包命名。

另一方面是领域行为描述。需要满足以下要求。从领域的角度而非实现角度描述领域行为,若涉及到领域术语,必须遵循术语表的规范强调动词的精确性,符合业务动作在该领域的合理性,还要突出与领域行为有关的领域概念。

统一语言是团队中不同角色之间高效沟通协作的大前提。人类的语言是不完美的,往往都是有强烈的二义性的。二义性会带来很多笑话,也会带来一些麻烦,举个例子:例如在英文里 look out 往往是“危险”的意思, 但是表面上看 look out 却是“向外看”。如果你正在开车,你身边的金发美女老外跟你喊 look out ,你就真的扭头往外看,而不是小心的握紧方向盘开车,那就真的很危险了。

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

2、领域

下面介绍各种域的概念。大家看下面这个图, 感觉这个图像是什么呢?有什么样的感觉呢?

DDD-领域
DDD-领域

最开始看到这个图的时候,我想到的就是构成生命体的最重要的一种物质或者结构: 细胞体。软件的架构方式,应该像生命体一样,有清晰的“细胞体”“细胞壁”“细胞核”等元素,才称得上是有生命力的的软件。

Domain,即领域,就是用于确定范围的,范围即边界,这也是 DDD 在设计中不断强调边界的原因。简言之,DDD 的领域就是这个边界内要解决的业务问题域。边界是非常重要的。例如 :一个 CRM 系统、一个 HR 系统、一个电子商务的商城,都可以作为领域概念。 如果一个软件公司,既提供 CRM 的 SAAS 服务,也提供 e-HR 的 SAAS 服务,但由于这两种业务系统是有很明确的边界,那么这两个业务就要各自独立为两个不同的领域。不能因为是同一家公司的产品,就要混在一个领域范围内。

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

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

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

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

那么为什么要区分这么多不同的域?极客时间专栏欧创新的《DDD 实战课》里面的解释和举例非常通俗易懂:

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

3、限界上下文

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

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

所以语言是离不开语义环境的,任何的“统一语言”一定要建立在明确的“限界上下文”之中,才能是真正的统一语言。在我们做系统设计的时候, 限界上下文的切分非常重要。这边 Bounded 的边界,要不大不小正合适。一般来说的原则如下图:

DDD-限界上下文
DDD-限界上下文

 

举个例子,在CRM的销售自动化领域中, L2O  和  O2C  两个阶段就是典型的两个很自治的子域。L2O 一般从公海和线索开始,到生成客户和商机并生成了订单,是一个售前的相对很完整闭环的流程,很适合划分为一个独立的限界上下文。O2C 是指从订单到回款和应收阶段的完整闭环的售中和售后流程,也非常适合单独划分为独立的限界上下文。

4、上下文映射

什么是上下文映射? 上下文映射是来讨论限界上下文之间的协作问题。 两个不同的限界上下文之间也有关系,而且这个关系还有方向。一般为两种关系: 上游(Upstream),下游(Downstream)。 在上下文映射图中,以 U 代表上游,D 代表下游。如下图所示:

DDD-上下文映射
DDD-上下文映射

 

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

三、战术设计

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

1、贫血模型和充血模型

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

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

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

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

2、实体和值对象

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

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

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

先说实体,一个典型的实体具有三个要素:身份标识,属性, 领域行为。

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

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

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

实体说完了,什么是值对象呢?

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

DDD-值对象示例代码
DDD-值对象示例代码

Money 值对象的定义就保证了它的不变性,另外,值对象一般是需要依附于某一个实体来存在, 对依附于实体的值对象,称之为“内嵌对象”。如下图示:

DDD-实体值对象
DDD-实体值对象

3、聚合和聚合根

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

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

4、领域服务

对于一些复杂的业务,需要跨多个实体和值对象来进行业务操作,这时候,如果把相关操作封装在某个单一的实体中,是不合适的。这时候需要引入领域服务的概念。领域服务会对多个实体和实体方法进行组合和编排,供应用服务调用。如果它需要暴露给用户接口层,领域服务就需要封装成应用服务。

5、资源库(Repository)

资源库(Repository)是对数据访问的一种业务抽象,使其具有业务意义。利用资源库抽象,可以解耦领域层与外部资源,使领域层变得更为纯粹,能够脱离外部资源而单独存在。资源库的概念跟之前经典的三层结构的 DAO 层有类似之处。不过资源库的概念更加的抽象,如下图:

DDD-资源库
DDD-资源库

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

6、领域事件

领域事件是指,发生后通常会导致进一步的业务操作的事件。举个例子:在 XXX 电商平台下单购买商品支付成功后, 支付成功就是一个领域事件。后续会导致进一步的例如减库存、发货、开票等一系列业务操作。领域事件的技术实现方案,一般都是通过异步的消息来描述,是很典型的适用适用各种 MQ 之类的消息中间件的场景。

四、事件风暴 Event Storming

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

其实在最开始 DDD 的概念中,没有事件风暴 Event Storming 这个概念,这个概念后来由 Alberto 提出来 ,详见: https://www.eventstorming.com/

事件风暴不是某种具体的做架构设计的方式或模式。而是一种类似于头脑风暴似的会议方式。国内公司可能做这个比较少,外企比较流行。这样的形式,可以让不同角色的人,在一起将业务场景做好梳理,并将要构建的系统中的核心概念、流程、动作、事件都做出清晰的梳理。

一般事件风暴的做法是这样:1、业务人员、领域专家、技术人员、架构师、测试人员都要参与;2、准备多种颜色的即时贴。3、在一个开放的空间,最好有一面有很大很宽的白板墙,以便于大家互相讨论并把各种重要的概念通过即时贴体现在墙上。Thoughtworks 的人也把这个过程戏称为“糊墙”。以下是一次比较成功的事件风暴后的成果图:

DDD-事件风暴
DDD-事件风暴

一般来说,经过一次或者几次的事件风暴后, 整个系统的战略设计的架构也就呼之欲出了。而战术设计层面的各种实体和值对象,聚合和聚合根以及领域事件也就基本被识别出来了。更多事件风暴信息见:https://github.com/mariuszgil/awesome-eventstorming

五、DDD  指导应用

1、DDD 和微服务

由于 DDD 的限界上下文的概念,能够很好的指导微服务的拆分粒度,避免了微服务架构下,服务拆分的过粗或者过细的问题,所以 DDD 和微服务可以结合在一起。

2、DDD 和敏捷

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

DDD 和敏捷
DDD 和敏捷

关于敏捷,还有一个理解,  即敏捷是一种状态,是形容词,不是名词。 如果能够快速的响应各种变化,才是真正的敏捷。 采用敏捷框架,只是名义上的敏捷罢了。 

3、DDD和中台

中台的概念,大火于2018年, 但当时大家大多是讨论这个概念, 说的是 WHAT 和 WHY。2019 年后,中台理念趋于务实和成熟。这时候大家都在思考的是 HOW。到了 2020 年,DDD 的理念让中台概念有效的在企业内落地。

中台建设是一定要聚焦于领域模型上,中台不是目的,而是手段,业务才是目的。只有领域模型梳理的足够清晰,才能够有的放矢的把中台建设好,并发挥出应有的作用。

4、DDD 落地过程

  • 先做战略设计:明确统一语言,划分各种域,理清楚限界上下文直接的依赖关系,做好分层设计架构。(这个过程 UML 或者 4+1 视图,是必不可少的)
  • 战略设计结合事件风暴,把设计进一步细化,衍生出下一步的战术设计:实体、值对象、聚合、聚合根等。
  • 基于上两步,开始工程化的代码工作。每一个限界上下文可以作为独立部署的微服务或者是工程,实现领域模型的充血模型的代码实现, 资源库,工厂,领域服务的代码实现。

(全文完)

《干货分享:关于 DDD 的那些事儿》的相关评论

发表评论

必填项已用 * 标记,邮箱地址不会被公开。