DDD学习笔记与实战

为什么DDD难学

2018年,随着阿里推出中台战略,各个公司都对标阿里中台的概念,一套行之有效的中台搭建理论就急需出现,DDD正好是一套可以支持搭建并拆分中台微服务的思想方法论。但实际上,DDD的提出要早于中台理论20年的。DDD从提出来就是一种不温不火的方法论,它指导我们按照业务逻辑,业务过程,不断地将业务归纳总结为一个一个的模块,之后按照模块的具体操作来划分为操作步骤的。本身他就是一种面向对象的思维,再加上其战略过程和人的思考过程一致。所以他不难。

然而DDD难的地方在于,正是因为它是一种指导编码的思维模式,毕竟“每一人心中都有一个哈姆雷特”,所以仁者见仁,智者见智,每个人的理解都不一样,但是又都是对的;再加上操作上不同领域的人对于业务有不同的着重点,会划分不同的模块,所以会让我们感觉DDD有点难。

本文目的

文章是根据我所阅读的DDD文章结合自己的感悟所写的,但其实我了解DDD也不是很长,所以可能有些地方用词不准,但我尽量使自己看的明白,也使别人看得明白,毕竟,知道了是一个层次,说明白又是另一个层次。

我希望我我再一次读到这个文章的时候,会让我有更深的认识并追加在文章上,也希望我在实际使用的时候,能更加明确DDD理论的目的,而不是拘泥于写法,拘泥于分层。我也希望,我可以按照自己所述的一些方法,轻松的划分业务模块,搭建代码。

其次我希望通过学习,我能真正理解DDD架构的精髓:
从业务视角出发。
由内而外构建业务服务。
注重防腐,保证领域服务内部的稳定性。

我认为DDD的目的是很明确的,它旨在解决软件核心领域的复杂性问题,也就是说,让我们构建项目时明确,拆分项目时轻松。专业一点的说法就是让每一个模块在其自身领域内“高聚合,低耦合”。

总述

DDD:领域驱动设计(Domain Driven Design),简称DDD。是一种用于构建中台及微服务的行之有效的设计方法。DDD战略可以根据业务亲和度和代码亲和度分为战略设计战术设计战略设计的设计关注点在与业务,根据业务来实现,而战术设计的设计关注点是以战略设计结果为基础,来进行代码方面的设计。

下面我们就以一个考勤系统来实践:
公司为了实现人力资源方面的计算,将通勤作为一个独立的系统,但是由于通勤审批规则上的繁杂,会频繁穿插人员部门的查询和人员上级的查询。我们希望可以使用DDD来将通勤系统重构,达到以通勤和员工为核心的DDD分布式结构。

战略设计

接下来先介绍相关的战略计划。战略计划中会有一些比较专业的词汇,我们从这些专业词汇来讲,通过这些专业词汇来探索,DDD是如何对一个业务进行划分的,是如何命名的,又是如何来使用的。

名词解析

1、领域

DDD中,领域指的是对与专业事务的范围,领域既可以表示整合业务系统,也可以表示其中的某个核心域或者支撑域。
我这么理解,以人才资源系统下的通勤系统来说,在这个领域内,我们有人员登录、请假、考勤三个大的模块,而薪酬系统又是人才资源系统下相对于通勤系统的另一个领域。
当然在通勤系统下我们也可以说,登录会有登陆的领域,请假会有请假的领域,考勤会有考勤的领域,因为角度不一样,当前业务范围不一样,所以考察的点不一样。有些项目的领域界限可能就只是人才系统,而有些项目随着功能的增加,业务对于项目进一步的要求,需要把人才系统划分成一个又一个的新的领域。

2、子域

子域同样是领域中的一种,不过确实依附于某一个领域中的,如果不存在其领域,那也就不存在其子域。
同样的,以通勤系统来说,在通勤领域下,我们可以说存在有登录,请假,考勤三个子域。这种划分方式,可以,但没必要。
一个程序员对于子域的划分一般是以子域在领域中的功能以及重要程度来划分的,当然这个功能和重要程度对于各个领域来说又不完全一致。
比如:通勤领域下,登录子域就显得不是那么的重要,因为理论上登录功能完全可以提出来供请假和考勤一起使用,也可以供通勤领域外的领域使用。所以我们一般把这种通用性子域成为通用域。而考勤子域和请假子域就显得比较重要了,可以说是项目的核心内容,没了这些内容,项目存在的必要性就不复存在了,对于这种决定了项目乃至公司的核心技术和核心竞争力的子域我们叫它为核心域,而在设计过程之中,会存在一部分功能,他既不属于通用域,更不属于核心域,我们通常称这部分组成起来的子域叫做支撑域,见名识意,支撑域一般可能没有什么具体的业务含义,旨在支撑项目的正常运行,比如我们的持久化技术,消息通道,缓存等,一般这种工作的生命周期都会在支撑域实现。

3、限界上下文

限界上下文是DDD设计中提出来的一种新的概念,这个概念我发现好多文章不是长编大论就是模糊不清,我认为一个好的概念应该是可以用一句话解释清楚的,否则这个概念就应该被拆分。
抛去百度谷歌的搜索情况,我对限界上下文这样定义,限界上下文是一种领域内的通用语言,旨在定义领域内的专业术语,并使用这些专业术语通过事件风暴(激烈的讨论)来获取领域的准确定位和其子域的准确定位。一个限界上下文一般对应一个微服务简单点就是一种格式话术,通过这种话术来为各个定义好的领域增加定语,宾语甚至从句。
比如:
为了(员工考勤计算)的目的来设计(考勤)系统。实现这个系统需要有(员工的请假,员工的考勤)的功能。
因为员工请假和考勤在业务上还可以进行划分,所以
为了(员工请假)的目的来设计(请假)系统。实现这个系统需要有(创建请假单,修改请假单,审批,发布请假单)的功能。
到此我们发现功能不可拆分,过度拆分会引起运维上的困难,所以我们认为员工请假就是一个限界上下文,我们希望可以把他整理成为一个微服务。
在这里插入图片描述
此时利限界上下文我们已经可以把领域给划分成不同的子域

4、实体与值对象

实体与值对象就比较好理解了,实体指代的是真正意义上看得见摸得着的抽象对象。实体在代码中会有一个ID来标识其唯一性。实体在我们的代码中会以领域对象DO的形式存在。而值对象存在有两种形式,一种是实体属性另一种是序列化子串;值对象的存在意义很大程度上处决于代码的编写形式,比如:如果我们持久化中的部分属性只做查询结果使用,这种情况下值对象会以序列化字符的形式存入到数据库中,当然,我们在表述一些状态值或者范围性属性值的时候,值对象会以实体属性的形式出现。同样的,值对象的缺点也是有的,就是他在作为搜索和过滤条件的时候可能并不是很方便。
在我们的开发中,免不了需要使用方法来计算实体。方法一般在领域模型中叫做命令

实体与值对象的特点

实体的特点:有ID标识,通过ID判断实体之间的相等性。ID在聚合内部唯一。状态可变并依附于聚合根,其生命周期应该交予聚合根进行管理,实体一般会进行持久化,但是不一定会与持久化对象一一对应,另外实体可以应用聚合内的聚合根、实体和值对象。
值对象的特点:无ID,不可变,无生命周期,用完即可回收。值对象之间的相等性应该由值对象的属性进行判断,其本质是一组完成的属性组成的集合,用于描述实体的状态和特征。值对象尽量只引用值对象。

5、聚合

**聚合指的是一系列有关系的实体、值对象、命令组合而成的。一个子域中可能会存在有多个聚合,而识别聚合的唯一实体,我们称作为聚合根。**有些情况下,聚合根是无法进行存在的,这个时候我们会以集中式架构中的管理方式来识别聚合。另外,聚合通常是拆分微服务的最小单位。

聚合的设计

聚合作为领域模型中最低层的边界,导致聚合的设计决定了一个项目能走多远。因此一个聚合的设计是尤为重要的。一个聚合中理因包括:聚合根(作为聚合的唯一标识),实体和值对象(用于秒数据和的各种属性),命令(表明聚合目前所能做的服务),领域服务(聚合所能做服务的具体表现)。包括个元素之间的依赖关系。将这些元素设计好是不容易的,前辈们总结出来一系列设计聚合的原则:

  1. 在一致性边界内建模真正的不变条件:这里所说的不变条件指的是聚合内部各实体值对象的相对静止,聚合内部应该有一套不变的业务规则,各实体和值对象按照这种统一的业务规则运行。实现聚合内部数据的强一致性,边界之外的东西都与该聚合无关,以达到聚合高内聚的性质。我这里简单的理解为一个聚合应该是对应有一个事务,用事务来保障数据的强一致性。
  2. 设计小聚合。小聚合可以保证实体与值对象的轻松管理,并且在高频高并发的情况下,减少并发冲突和数据库锁的出现,增强系统的可用性,达到降低由于业务过大导致的聚合重构。
  3. 通过唯一标识引用其他聚合。聚合与聚合之间通过关联外部聚合根放入方式引用,而不去做直接的对象引用。将外部聚合的对象放在内部管理会导致聚合边界不清晰的问题,容易回归到OSA架构的设计上,也会相应的降低聚合之间的耦合度。
  4. 边界之外最终一致性。聚合内应该保证事务的强一致性,但是如果遇到聚合和聚合之间的一致性问题,需要保证聚合之间的最终一致性即可。最终一致性我们一般采用领域事件的方式异步修改相关的聚合,实现聚合之间的解耦。
  5. 通过应用层实现跨聚合的调用。为了实现微服务内聚合的解耦,以及未来拆分聚合作为微服务,应该避免聚合之间的领域服务调用和跨聚合的数据库表关联。

聚合与聚合根的特点

聚合的特点:高内聚低耦合,微服务拆分的最小单位。一般不建议过度对微服务进行拆分。但是在部分极致要求下可以使聚合作为一个独立的微服务,来满足高频的发布和极致的弹性伸缩能力。
聚合根的特点:聚合根本质上是特殊的实体,它应该具有实体的特点。除此之外,他应该具体有全局唯一标识与独立的生命周期。一个聚合只能有一个聚合根。聚合根在聚合内部,应该使用对象应用的方式来管理实体和值对象,在聚合之间,应该通过ID关联的方式实现聚合之间的协调。

关系图

总的来说,以上专业名词之间的关系符合一下图示:
在这里插入图片描述

战略设计开始

战略设计分为事件风暴和领域事件的讨论,事件风暴会确立项目在业务上的聚合与领域,而领域事件则会确立聚合与聚合,微服务与微服务之间的关联性。

事件风暴

我们知晓了DDD的一些关键名词之后,紧接着就利用DDD来对考勤系统进行业务重组,这个环节在DDD中叫做事件风暴。事件风暴旨在业务人员、领域专家和开发人员共同讨论出领域、子域、聚合、实体、值对象、命令等元素。将这些元素建立关系,构建领域模型。

考勤系统中我们通过事件风暴得出以下结论式表格:
在这里插入图片描述
我们将考勤系统作为一个领域,分解出请假和人员两个聚合,聚合下分别以请假单和人员为聚合根,实现创建请假信息、创建审批轨迹信息、创建人员信息、创建组织关系的命令,并希望其在领域服务中实现。

领域事件

事件风暴之后,我们总结出了不同聚合,以及不同聚合内的聚合根、实体、值对象、命令、服务等,但是我们在事件风暴时,不免会发现,事件发生后,通常会导致进一步的业务操作,这种事件我们称之为领域事件。

通常如果我们在描述业务的时候,说了:“如果发生了。。。。,就。。。。”,或者“当做完。。。。的时候就去通知。。。。”诸如此类的描述时,那很有可能就是触发了领域事件。领域事件发生在两个聚合之间,根据聚合设计原则:边界之外最终一致性,发生领域事件会采用异步方式来保证最终一致性,以此来切断事件和事件之间的强依赖性,发生领域事件时,一个事件的终结,往往以事件的发布最为结点,这就表明,事件发布之后,不会在意后续订阅方的处理是否成功,这样聚合之间就可以达到解耦的目的,并维护了聚合的独立性和数据一致性。
比如:当我们需要修改请假单中的请假时间时,我们会校验当前用户是否可以完成修改,此时我们会得到两个事件,事件一用来判断用户是否可以修改,事件二用来保存修改后的请假时间。事件一只会在乎事件是否发送,至于事件二,对于事件一的结算过程也毫不CARE,他只会捕捉事件一的事件结果,一旦捕捉到了就会执行事件二。这样事件一和事件二就达到了人员和请假两个聚合之间解耦的目的。(我认为这个例子还是在事件之间具有强关联性,有一个例子举得很好:购物车的推送
我认为,事件风暴讨论的结果,最好的表达方式是以表格或者领域模型图来说明的,领域事件最好的表达方式应该是以聚合中领域服务的名称为流程单位而表达的流程图。
在这里插入图片描述

战术设计开始

经过战略设计,确认聚合与领域以及相关事件的关联性之后,面临的就是战术设计。战术设计分为:、、、、几个阶段,通过战术设计,代码结构一般就会浮出水面。

微服务架构的选择

首先要知道架构是什么。架构的本质,简单来说,就是要素结构。所谓的要素(Components)是指架构中的主要元素,结构是指要素之间的相互关系(Relationship)。其次要清楚,常用的微服务架构有哪几种。都有什么特点。知道了这些架构的特点与架构之间的区别,才能更好地选择架构。

整洁架构(洋葱架构)

在这里插入图片描述
2008年Jeffrey Palermo已经提出了具有分层思想的洋葱架构,如上图,同心圆代表软件的不同部分,从里向外依次是领域模型,领域服务,应用服务和外层的基础设施和用户终端。
洋葱架构根据依赖原则,定义了各层的依赖关系,越往里依赖程度越低,代码级别越高,越是核心能力。外圆代码依赖只能指向内圆,内圆不需要知道外圆的情况,这种架构也是典型的分层架构,和DDD分层架构一样,都体现了高内聚,低耦合的设计特性。洋葱架构也常作为指导微服务设计的重要架构之一。

  • 领域模型实现领域内核心业务逻辑,它封装了企业级的业务规则。领域模型的主体是实体,一个实体可以是一个带方法的对象,也可以是一个数据结构和方法集合。
  • 领域服务实现涉及多个实体的复杂业务逻辑。
  • 应用服务实现与用户操作相关的服务组合与编排,它包含了应用特有的业务流程规则,封装和实现了系统所有用例。
  • 最外层主要提供适配的能力,适配能力分为主动适配和被动适配。主动适配主要实现外部用户、网页、批处理和自动化测试等对内层业务逻辑访问适配。被动适配主要是实现核心业务逻辑对基础资源访问的适配,比如数据库、缓存、文件系统和消息中间件等。
  • 红圈内的领域模型、领域服务和应用服务一起组成软件核心业务能力。
六边形架构(端口适配器架构)

2005年Alistair Cockburn提出了六边形架构,在这个架构中,将应用分为内六边形和外六边形两层,内六边形实现应用的核心业务逻辑。外六边形完成外部应用、基础资源等的交互和访问,对于与不同的外部系统交互,由外六边形的适配器负责协议转换,保证内六边形业务逻辑的干净。
这种架构也是典型的分层架构,和DDD分层架构一样,都体现了高内聚,低耦合的设计特性。六边形也常作为指导微服务设计的重要架构之一。
在这里插入图片描述
六边形架构的核心理念是:应用是通过端口与外部进行交互的。
在上图的六边形架构中,应用程序和领域模型与外部资源(包括APP、Web应用以及数据库资源等)完全隔离,仅通过适配器进行交互。它解决了业务逻辑与用户界面的代码交错问题,很好地实现了前后端分离。除此之外,六边形架构各层的依赖关系与整洁架构一样,都是由外向内依赖。

DDD分层式架构(四层)

DDD分层式架构是由传统的SOA架构演变而来的。
首先需要提出DDD分层式架构最重要的原则是:每层只能与位于其下方的层发生耦合。
在这里插入图片描述
如上图DDD分层式架构可以简单的理解为依赖倒置的传统式架构。图中箭头方向代表的是调用和依赖的意思。传统架构中的依赖关系如果以DDD的原则来看其实是以基础层为核心领域的,这是不对的,所以,我们应该以领域层为核心实现各层级之间的依赖以达到解耦的目的。
这也是为什么用DDD来设计的项目大多是以每一层作为jar用分jar包的形式来编写,而不是以package的形式来编写,这是为了使用Maven依赖来强制达到层级之间的依赖关系。
将上图进行优化设计,便是网上最常见的DDD分层架构(DDD四层架构)。
关于DDD分层架构的发展请见
在这里插入图片描述
上图所示,用于在各层之间传输数据的类型一般属于以下类型

  • PO(数据持久化对象):与数据库字段映射的数据载体
  • DO(领域对象):领域模型核心业务对象的载体,包括实体和值对象
  • DTO(数据传输对象):用于前端和微服务交互的数据传输载体
    各层担负以下内容:
  1. 用户接口层:主要有facade接口,Assembler转换器
    微服务面向不同前端时,需要展示的数据可能不同,此时由于需要保持领域核心业务逻辑的稳定,不可能去定制开发各种领域服务和应用服务编排。因此,为避免暴露服务端业务逻辑,防止非必需的字段数据外泄 ,同时保证领域逻辑的干净,用户接口层的facade接口和Assembler转换器就发挥作用了。
    facade接口用于封装应用服务,适配不同前端需要的字段,提供不同要求的服务接口适配。Assembler根据不同前端的数据请求,完成DTO和领域DO对象的组装,转换,完成数据适配。
  2. 应用层:
    应用层连接用户接口层和领域层,主要协调领域层,面向用例和业务流程,协调多个聚合完成服务的组合和编排,在这一层不实现任何业务逻辑,只是很薄的一层。
    应用层编排成应用服务后,被接口层facade封装,完成接口和数据适配后,以粗粒度向API网关发布服务。
    应用层还负责事件的订阅和发布,以及与其他外部服务的交互,事件的具体实现则在领域层。
    防腐层:防腐层是DDD提出的应用于解耦方面的一种层级观念。简单说,就是应用不要直接依赖外域的信息,要把外域的信息转换成自己领域上下文(Context)的实体再去使用,从而实现本域和外部依赖的解耦。
  3. 领域层:
    领域层位于应用层之下,是领域模型的核心,主要实现领域模型的核心业务逻辑,体现领域模型的业务能力。
    领域层关注实现领域对象的充血模型和聚合本身的原子业务逻辑,至于用户操作和业务流程,则交给应用层去编排。这样设计可以保证领域模型不容易受外部需求变化的影响,保证领域模型的稳定。
    跨多个聚合的领域逻辑在领域层实现,由领域服务组织和协调多聚合的多实体,实现原子业务逻辑。
  4. 基础层:
    基础层贯穿了DDD所有层,包括第三方工具,API网关,消息中间件,分布式事务,消息最终一致性能力,数据库,缓存能能力的提供。
    基础层有仓储模式的代码逻辑,通过仓储接口和仓储实现,解耦领域层和基础层,保证领域核心业务逻辑的干净,降低DB资源变化给领域层带来的影响。
COLA

COLA是我国阿里研究开发的一套框架,我个人认为COLA是DDD思想指导实践之后,总结并沉淀出来的一套应用框架。COLA框架的分层如下:
在这里插入图片描述
总体来说,COLA的分层架构和DDD分层架构是很像的。图中的Gateway旨在建立Domain层和基础层中的持久化操作之间的桥梁。
COLA框架的划分维度分为两个:领域划分和功能划分。
在这里插入图片描述
从外层看,框架是以领域划分的,但是在层中,又是分别以功能进行拆分的,这样在达到解耦的同时也有利用将来项目的拆分。具体使用以及介绍可以参考文档COLA 4.0

战略设计的业务翻译

在这里插入图片描述

代码DEMO

请假系统并没有实际的写过,我们直接编写了需要重构的项目,由于版权问题,无法上代码,但是我自己写了一个分层式框架,仅供参考(“每一人心中都有一个哈姆雷特”)。

将传统项目重构为DDD

首先我们需要知道是不是真的需要重构项目,毕竟DDD的编写是为了便于日后的拆解,所以为了将层与层之间隔离开来,DDD要求在每一层都封装自己的类型,应用层之间的沟通依赖于消息通知,本质上他的编写是比较繁琐的。

如果确认了需要使用DDD进行指导和架构,那就刚。

目前大多数项目都是三层式的项目,如果原始项目本身在service层已经做好了内聚,那么我们直接对应下图来构建过新项目即可
在这里插入图片描述
如果原始项目在service的内聚并不是很高,我们需要重头精炼模型,通过事件风暴,按照战略设计到战术设计的过程,抽象内聚,编写DDD分层文档,最后落实代码。

其实在真正的实践过程中我们发现,DDD的领域模型往往是多变的,不定向的,因为业务在发展,相应的模型也在发展,但是发展过程中,模型始终会趋于稳定,在实践中创建一个DDD架构项目,往往没有精炼模型,创建领域模型来的重要。

实践与问题

  1. 到底什么是充血模式
    对于这个问题,目前我们是有分歧的,在实践中我们往往会综合考虑,目前问题的答案是分为两种说法的。
    第一种说法,充血模型指的是在实体中编写对象属性以及其对应的相关操作,DDD理念强调对象不单单具有属性,还应该具有属性的相关操作,比如实体“请假单”不止包含其所具备的属性,还应该包含请假单所具备的操作,比如创建请假单,通过这种手段来达领域内聚。
    第二种说法是:充血模型是指每一个实体模型必须达到高度充实,即实体中不应该存在空的,或者模棱两可的属性,如果一个实体中存在有空值属性,那么说明实体的划分不够精确,需要再度划分。通过实体本身的充血,来达到实体的高度聚合,进而达到领域内聚。
  2. DDD是不是"银弹"
    对于业务简单且偏查询的项目不适合,如果使用反而会增加系统的复杂度,徒增开发成本。但DDD的优势在于业务模型和技术实现相一致。修改模型就是修改代码,代码重构就是提炼模型。
  3. jar式结构还是package式结构
    建议使用JAR式结构,通过maven的依赖,强制使依赖倒置,一次达到领域内的聚合和干净。
  4. 防腐层和通用层
    防腐层一般是建立与应用层内的,主要目的是隔离上游的功能,防腐层通过已有接口和外部系统交互,在内部做己方和他方模型的转换。
    简单来说,防腐层一般为了隔离两个系统之间的变化,防止一个系统的微小变化会影响到另外一个系统;还有一个场景,两个系统的技术栈不一致,所以需要一层代理来兼容。
    环境层(DDD分层架构-五层架构中的内容)主要是用来隔离应用层和领域层的,当发生领域模型需要拆分为独立系统时可以当做防腐层编辑。
  5. 到底什么是依赖倒置
    依赖倒置的主要目的是为了实现领域层的内聚,即将具体的持久化等内容交由基础层实现,而不在环境层中多做处理。
    依赖倒置的主要做法是:在领域层中建立接口,在基础层中实现接口。

参考文档

DDD下的各种架构
COLA 4.0
DDD 概念参考
Spring Boot Event 观察者模式,轻松实现业务解耦!
领域驱动设计对软件复杂度的应对
ddd 企业应用架构模式_DDD分层架构的三种模式

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值