DDD(中)
DDD 建模方法
四色建模(风暴事件)是整个 DDD 这套软件设计方法中用于工程拆分界限上下文的非常重要的实践手段。通过建模过程快速识别业务领域中的关键事件和核心流程,也是在这个过程中设计出领域对象的,为后面详细设计和代码开发做指导。
可以把整个过程理解为,为工程开发提供面向对象设计,涵盖;领域拆分、界限串联、功能聚合。所以相比Service + 数据模型
的贫血开发方式,DDD 前期需要付出更多的设计成本,但对于软件的长周期迭代,这样的好处是非常大的。
1. 建模目的
工程的建模的目的是为了我们做工程开发时提供指导方案,就像一栋大楼的设计蓝图一样,也像一个超市中会有不同品类的货架,需要提前规划好。所以你需要在工程开发时所需的各类核心内容,都会在建模中体现,如;分几个包、有哪些核心对象、要串联什么流程、有哪些核心业务要实现、过程中与外部服务的交互。
那么为了达成一个讨论的共识,而不是每个人都有一套的标准和词汇。所以会使用 DDD 提供专门的建模方法和名词进行统一的设计,此外因为 DDD 的统一建模语言,不涉及技术编码,也具有通用性,所以可以在建模过程让产品、研发、测试、架构师等人员一起参与讨论。如;领域、领域模型(实体、聚合、值对象)、领域服务、端口适配器、仓储、界限上下文、领域编排等名词。
2. 怎么建模
DDD 的建模过程,是以一个用户为起点,通过行为命令,发起行为动作,串联整个业务。而这个用户的起点最初来自于用例图的分析。用例图是用户与系统交互的最简表示形式,展现了用户和与他相关的用例之间的关系。通过用例图,我们可以分析出所有的行为动作。
在 DDD 中用于完成用户的行为命令和动作分析的过程,是一个四色建模的过程,也称作风暴模型。在使用 DDD 的标准对系统建模前,一堆人要先了解 DDD 的操作手段,这样才能让产品、研发、测试、运营等了解业务的伙伴,都能在同一个语言下完成系统建模。
此图是整个四色建模的指导图,通过寻找领域事件,发起事件命令,完成领域事件的过程,完成 DDD 工程建模。
- 蓝色 - 决策命令,是用户发起的行为动作,如;开始签到、开始抽奖、查看额度等。
- 黄色 - 领域事件,过去时态描述。如;签到完成、抽奖完成、奖品发放完成。它所阐述的都是这个领域要完成的终态。
- 粉色 - 外部系统,如你的系统需要调用外部的接口完成流程。
- 红色 - 业务流程,用于串联决策命令到领域事件,所实现的业务流程。一些简单的场景则直接有决策命令到领域事件就可以了。
- 绿色 - 只读模型,做一些读取数据的动作,没有写库的操作。
- 棕色 - 领域对象,每个决策命令的发起,都是含有一个对应的领域对象。
👩🏻🏫敲黑板 综上,左下角的示意图。是一个用户,通过一个策略命令,使用领域对象,通过业务流程,完成2个领域事件,调用1次外部接口个过程。我们在整个 DDD 建模过程中,就是在寻找这些节点。
3. 超市举例
我们先通过一个真实场景的案例,代入下 DDD 四色建模的术语,这样可以更有益大家对四色建模理解。这个场景是一个在超市购物的场景。想象下,一个男人,得到了老婆的”命令”,去大超市拿着空的购物车,推进去一圈圈的走,最终完成购物打车回家。
- 脑子中的想法,是媳妇给下达的指令。我们可以理解为决策命令。行为人带着决策命令来到超市。
- 购物车是一种实体,需要填充数据的实体。行为人,带着实体,进入到超市中从各个货架选购商品,装入购物车。选购商品为业务流程,装满的购物车为领域事件。也就是最终态,完成媳妇交代的任务。而手里的烟,则是另外一个领域事件。也就是说,一次的行为动作可以完成多个领域事件。
- 最终购物完成后,打车回家。则是下一个领域流程。通过把从加入出门、做地铁、进超市、采购、打车回家,一整条领域串联起来就是领域编排
说白了,首先得整理清楚业务的执行过程,就像函数(决策命令)一样,入参(领域对象、业务实体)是什么,出参(领域事件)是什么,逻辑(业务逻辑)是什么,还需要用到其他什么(外部系统)
4. 业务案例
接下来,我们在以一个真实的业务场景来分析系统的四色建模过程。
1. 产品诉求
如图,是一个复杂的营销抽奖场景玩法需求,涵盖了;活动配置
、签到&奖励
、活动账户
、抽奖策略「责任链+规则树」
、库存扣减
、抽奖满N次后阶梯抽奖
等。面对这样的复杂系统,非常适合使用 DDD 落地
分析需求;
- 整体概率相加,总和为1或者分值计算,概率范围千分位
- 抽奖为免费抽奖次数 + 用户消耗个人积分抽奖
- 抽奖活动可给用户分配可抽奖次数,通过点击签到发放
- 活动延伸配置用户库存消耗管理,单独提供表配置各类库存;用户可用总库存、用户可用日库存
- 部分抽奖规则,需要抽奖n次后解锁,才能有机会抽取
- 抽奖完成增加(运气值/积分值/抽奖次数)记录,让用户获得奖品。
- 奖品对接,自身的积分、内部系统的奖品
- 随机积分,发给你积分。
- 黑名单用户抽奖,则给予固定的奖品。
2. 用例图
根据业务需求画系统用例图;
- 用例图(英语:use case diagram)是用户与系统交互的最简表示形式,展现了用户和与他相关的用例之间的关系。通过用例图,人们可以获知系统不同种类的用户和用例。用例图也经常和其他图表配合使用。
- 用例图,也可以等同于是用户故事(英语:User story)(软件开发和项目管理中的常用术语),主旨是以日常语言或商务用语撰写句子,是一段简单的功能表述。以客户或使用者的观点撰写下有价值的功能、引导、框架来与使用者进行互动,进而推动工作进程。可以被认为是一种规格文件,但更精确而言,它代表客户的需求与方向。以该用户故事来反应对象在组织内的其工作职责、范围、需要进行的任务等。用户故事在敏捷开发方法中用来定义系统需要提供的功能和实现需求管理。
- 尽管用例本身会涉及大量细节和各种可能性,用例图却能提纲挈领地让人了解系统概况。它为“系统做什么”提供了简化了的图形表示,因此被誉为“搭建系统的蓝图”。
3. 寻找领域事件
接下来,大量的时间,都是在挖掘领域事件。这个过程就是一堆人头脑风暴的过程,避免错失流程节点。
- 根据产品 PRD 文档,一起开会梳理有哪些领域事件。其实大多数领域事件一个人都可以想到,只是有些部分小的场景和将来可能产生的事件不一定覆盖全。所以要通过产品、测试、以及团队的架构师,一起讨论。
- 像是整个大营销的抽奖会包括如图所列举的事件。在列举这个阶段,你用在乎格式。也可以是每个人准备好黄色便签纸,想到一个就贴到黑板上一个,只是穷举完成。—— 实际做DDD中,也是这样用便签纸贴黑板,所以用不同的颜色做区分。
4. 识别领域角色和对象
在确定了领域事件以后,接下来要做的就是通过决策命令串联领域事件,并填充上所需要的领域对象。这块的操作,新手可以分开处理,如先给领域事件添加决策命令、执行用户和领域对象,最后在串联流程。就像 事件风暴定义
中的示意一样。
- 首先,通过用户的行为动作,也就是决策命令,串联到对应的领域事件上。并对复杂的流程提供出红色的业务流程。
- 之后,为决策命令添加领域对象,每一个领域在整个流程中都起到了至关重要的作用。
5. 划分领域边界
有了识别出来的领域角色的流程,就可以非常容易的划分出领域边界了。先在事件风暴图上圈出领域边界,之后在单独提供领域划分。
5.1 圈出领域
5.2 领域边界
到这步咱们就可以获得整个项目中 DDD 的领域边界划分了。之后再往下就是具体的每个领域对象的详细设计和流程设计
DDD工程模型
凡是做到架构师岗位的,都是具有一定技术思维敏感性的,不会主观评价好和坏,但能推演出业务与技术的迭代发展被动熵增与减熵增的意识。
什么是系统的工程结构,工程框架的作用是什么?
工程结构的存在的作用目的,是为了承载工程系统开发的模型划分,定义工程服务开发过程中实施标准。说白了,就是你在工程实现时,在哪个层访问数据库、哪个层使用缓存、哪个层调用外部接口、哪个层做功能实现,这就是工程框架结构定义的目的。
但在 Service + 贫血模型
的三层结构开发指导下,是没有细分出面向对象工程结构设计的趋于划分的。所以在通常意义的 MVC 下,开发过程所有需要的内容,都会堆砌到 Service 实现类中。这也是为什么 DDD 领域驱动设计的落地工程结构,会出现;洋葱架构、整洁架构、菱形架构、六边形架构等这些架构模型。因为我们需要更细致的划分,来承载 DDD 设计概念中映射的领域、仓储、适配、编排、触发,并重视面向对象过程。—— 其实你一上学,学Java就开始学面向对象了,只不过一点点在忘记它。
一、为啥需要架构
说到开发代码为啥需要架构,就想买了个房子,为啥要隔出厨房、客厅、卧室、卫生间一样,核心目的就是让不同的职责分配到不同的区域内。虽然在代码中是没有马桶要放卫生间、沙发要放客厅、床要放卧室。但他有一些列的科目信息要引入到工程。
在工程开发时会涉及到的核心科目;
统一的异常、数据库的连接、日志的打印、外部服务的调用、消息的监听、任务的轮训以及服务的实现等一些列的东西要处理,分配到不同的工程包下承载。在 DDD 之前,我们一直用 MVC 的分层结构承接这些内容;
通用的、配置的、组件的、持久化的、内部的、外部的,在以往的单体应用时代开发下,其实是没有这么多东西的,那时候的工程结构都偏向于 Service + 贫血模型实现。
但随着微服务的演进,越来越多的内容被填充到工程中,这个时候你细心的查看架构,就会发现原本的 MVC 结构其实已经变的非常混乱了。一个 Service 中为了实现自己的功能,要引入一堆的东西,这些原子的功能与 Service 自身的服务耦合在一块。也导致了工程的维护成本越来越大。
这样的三层工程结构分配方式,对于要承载庞大的分布式技术栈体系显然是有点小马拉大车,三缸机带不动SUV一样
二、工程结构设计
2004年,Eric Evans 在发表了一部名为《Domain Driven Design》的著作。2005年 Alistair Cockburn 提出的“六边形关系图”理论,2008年 Jeffrey Palermo 提出了洋葱架构。虽然这些架构并不是专门为 DDD 而出,但巧的是这些架构都在 DDD 一书发表之后陆续推出新的架构模型。同时这些架构的分层设计方式也都与 DDD 非常契合,在这些架构下也可以很好的落地 DDD 设计方法。
无论是六边形架构,还是洋葱架构,或是张毅老师提到的南向网关/北向网关的菱形架构,他们的目标都是以领域服务为核心,隔离内部实现与外部资源的耦合。
在 DDD 分层架构下,以支撑 domain 核心领域实现拆分出基础设施(infrastructure),来承接对外部资源的调用。触发器(trigger)向外部提供服务。之后 app 为应用启动、api 为接口定义、types 为通用信息、case 为编排。
在这样一套结构下,用于开发工程的各项科目也可以被优雅的分配到各个分层结构了。相对于 Service + 数据模型的贫血模型结构,现在就主要以 domain 为核心开发业务功能,不会在 domain 工程模块下,引入其他各类外部组件了,这样就可以更加关心业务功能开发。
之后是这样的思想映射到工程中,常见的分层结构会有两套,一套是整洁分层,另外一套是六边形分层。
1. 整洁架构 - 工程结构
整洁架构的分包形式,会将所有的外部依赖使用和工程内要对外的,统一定义到适配器层。这里可以理解为对适配和对内适配。
2. 六边形架构 - 工程结构
六边形架构,会把本身提供到外部的放到trigger,让接口调用、消息监听、任务调度,都可以统一一个入口处理。而对于需要调用外部同类的能力统一放到 infrastructure 基础设施层,包括;数据库、缓存、配置、调用其他方的接口。
三、领域模型设计
虽然大家用的都是 DDD,也都有对应的模块和分包,但在细节之处还是会有一些差异。就像家里的家庭成员,衣服、裤子、鞋子,是所有人的衣服都放一起,还是每个人有独立的衣柜只放自己的。这块是有差异的。另外这东西没有绝对的好和坏,就像厨房里的碗筷是是放一起的,卫生间的马桶也是共用的,这说明分包也是需要按照最符合自己所需来设定。
1. 分包方式
如下,是两种分包方式;
- 方式1;DDD 领域科目类型分包,类型之下写每个业务逻辑。
- 方式2;业务领域分包,每个业务领域之下有自己所需的 DDD 领域科目。
比如,你现在一个工程下有用户、积分、抽奖、支付,(紧凑的聚合类微服务有时候更易于维护),那么这些包一种是分为独立的业务包方式2这种,另外一种就是大家都在一个坛子里吃饭,要啥去各个地方找。所以你更倾向于那种呢?
2. 领域模型
DDD 领域驱动设计的中心,主要在于领域模型的设计,以领域所需驱动功能实现和数据建模。一个领域服务下面会有多个领域模型,每个领域模型都是一个充血结构。一个领域模型 = 一个充血结构
- model 模型对象;
- aggreate:聚合对象,实体对象、值对象的协同组织,就是聚合对象。
- entity:实体对象,大多数情况下,实体对象(Entity)与数据库持久化对象(PO)是1v1的关系,但也有为了封装一些属性信息,会出现1vn的关系。
- valobj:值对象,通过对象属性值来识别的对象 By 《实现领域驱动设计》
- repository 仓储服务;从数据库等数据源中获取数据,传递的对象可以是聚合对象、实体对象,返回的结果可以是;实体对象、值对象。因为仓储服务是由基础层(infrastructure) 引用领域层(domain),是一种依赖倒置的结构,但它可以天然的隔离PO数据库持久化对象被引用。
- service 服务设计;这里要注意,不要以为定义了聚合对象,就把超越1个对象以外的逻辑,都封装到聚合中,这会让你的代码后期越来越难维护。聚合更应该注重的是和本对象相关的单一简单封装场景,而把一些重核心业务方到 service 里实现。此外;如果你的设计模式应用不佳,那么无论是领域驱动设计、测试驱动设计还是换了三层和四层架构,你的工程质量依然会非常差。
- 对象解释
- DTO 数据传输对象 (data transfer object),DAO与业务对象或数据访问对象的区别是:DTO的数据的变异子与访问子(mutator和accessor)、语法分析(parser)、序列化(serializer)时不会有任何存储、获取、序列化和反序列化的异常。即DTO是简单对象,不含任何业务逻辑,但可包含序列化和反序列化以用于传输数据。
四、分层调用链路
接下来我们把 DDD 的分层架构平铺展开,看看从一个接口的实现到各个模块分层中的调用链路关系是什么样的。这样在做自己的代码开发中也可以参考到应该把什么的功能分配到哪个模块中处理。
从APP层、触发器层、应用层,这三块主要对领域层的上下文逻辑封装、触发式(MQ、HTTP、JOB)使用,并最终在应用层中打包发布上线。这一部分的都是使用的处理,所以也不会有太复杂的操作。
当进入领域层开始,也是智力集中体现的开始了。所有你对工程的抽象能力,都在这一块区域体现。
1. 环境
- JDK 1.8
- Maven 3.8.6
- SpringBoot 2.7.2
- MySQL 5.7 - 如果你使用 8.0 记得更改 pom.xml 中的 mysql 引用
- Dubbo - https://cn.dubbo.apache.org/zh-cn/overview/mannual/java-sdk/reference-manual/registry/multicast/ (opens new window)文档&广播模式地址说明
2. 架构
- 源码:
https://gitcode.net/KnowledgePlanet/road-map/xfg-frame-ddd
(opens new window) - 树形:
安装 brew install tree
IntelliJ IDEA Terminal 使用 tree
. |
以上是整个🏭工程架构的 tree 树形图。整个工程由 xfg-frame-app 模的 SpringBoot 驱动。这里小傅哥在 domain 领域模型下提供了 order、rule、user 三个领域模块。并在每个模块下提供了对应的测试内容。这块是整个模型的重点,其他模块都可以通过测试看到这里的调用过程。
3. 领域
一个领域模型中包含3个部分;model、repository、service 三部分
- model 对象的定义 【含有;valobj = VO、entity、Aggregate】
- repository 仓储的定义【含有PO】
- service 服务实现
以上3个模块,一般也是大家在使用 DDD 时候最不容易理解的分层。比如 model 里还分为;valobj - 值对象、entity 实体对象、aggregates 聚合对象;
- 值对象:表示没有唯一标识的业务实体,例如商品的名称、描述、价格等。
- 实体对象:表示具有唯一标识的业务实体,例如订单、商品、用户等;
- 聚合对象:是一组相关的实体对象的根,用于保证实体对象之间的一致性和完整性;
关于model中各个对象的拆分,尤其是聚合的定义,会牵引着整个模型的设计。当然你可以在初期使用 DDD 的时候不用过分在意领域模型的设计,可以把整个 domain 下的一个个包当做充血模型结构,这样编写出来的代码也是非常适合维护的
4. 环境(开发/测试/上线)
源码:xfg-frame-ddd/pom.xml
<profile> |
- 定义环境;开发、测试、上线。
源码:xfg-frame-app/application.yml
spring: |
- 除了 pom 的配置,还需要在 application.yml 中指定环境。这样就可以对应的加载到;
application-dev.yml
、application-prod.yml
、application-test.yml
这样就可以很方便的加载对应的配置信息了。尤其是各个场景中切换会更加方便。
5. 切面
一个工程开发中,有时候可能会有很多的统一切面和启动配置的处理,这些内容都可以在 xfg-frame-app 完成。
源码:cn.bugstack.xfg.frame.aop.RateLimiterAop
|
使用
# 限流配置 |
- 这样你所有的通用配置,又和业务没有太大的关系的,就可以直接写到这里了。—— 具体可以参考代码