最近在课堂账号项目中用DDD方法论进行分层落地实践,结合之前对DDD内容学习,对DDD实践这块有了一些新的认知,和大家做下简要分享,理解不当之处请大家斧正
1. MartinFowler 对 DDD 描述
https://martinfowler.com/bliki/DomainDrivenDesign.html
DDD 的一个特别重要的部分是战略设计的概念 —— 如何将大型域组织成一个有界上下文的网络。在那之前,我还没有看到有人以任何令人信服的方式解决这个问题 — MartinFowler
2. 领域驱动设计 - 领域是什么,驱动什么,设计什么?
- 领域举例 1,研究桃树:划分根、茎、叶、花、果实、种子,营养器官又能继续划分组织,组织继续划分细胞,细胞继续划分…
- 领域举例 2:
- 整个电商领域很复杂,需要通过边界上下文划分业务场景,商品(商城领域)、货品(仓储领域)、物品(物流领域)
- 腾讯课堂同样是用户,不同场景用户也不同:老师(教学场景)、学员(听课场景)
- 技术上为何会有 DDD?
- 战略设计,为了应对复杂业务情况,会通过分治思想,将大的业务领域拆成多个子业务领域,每个子业务模型收敛;这点与微服务的思想不谋而合,即大单体拆成微服务(不复杂业务场景不建议使用 DDD,DDD 和微服务也没有必然联系,反模式,无论需求大小上来直接 DDD 设计)
- 好处:统一领域语言,降低沟通成本;降低领域复杂度
- 难点:对统筹整体的人要求很高,要有全局业务视角才能更合理的划分清楚业务
- 驱动什么,设计什么?
- DDD 让业务转技术有个方法论,驱动微服务设计的落地(常见微服务拆分得过细,微服务不够内聚的一个原因就是因为业务领域上下文边界模糊,没有划分清楚)
- DDD 本质?
- 是软件工程中软件建模的一种方法论,基于用户的用例(可以通过头脑风暴、用例场景描述产生)对相关的业务现实进行建模,最终能够指导微服务架构落地的一种方法论
3. DDD 落地步骤:先收集用例分析,然后领域设计,最后才是编码开发
https://domaindrivendesign.org/analytics-first-then-design-and-only-then-development/
- 先画 BPMN(业务流图)、UML Activity(实体活动图),与测试用例结合(可以和业务专家事件风暴,深入业务讨论),先把业务探讨清楚,比如一个用户下单场景:
- 商品加购物车上下文
- 用户添加商品购物车、删除购物、清空购物车(实体有用户、商品、购物车)
- 用户添加商品,增加商品、减少商品数量(实体有用户、商品)
- 订单结算上下文
- 用户虚拟信息相关(比如课点、优惠券)
- 优惠计算
- 订单风控
- 订单生单
- 订单支付上下文
- 虚拟资产扣减(课点、优惠券)
- 用户选择支付方式(实体有用户、支付方式)
- 商品加购物车上下文
- 通过步骤 1 对业务了解后:
- 发现有 3 个领域的上下文:购物车操作、订单结算、订单支付
- 拆解成 3 个领域服务跟进,对应微服务就是购物车、订单、支付 3 个微服务
- 步骤 1 实际有发现领域内还有用户、商品等信息,表明购物车实际也是对其他领域服务有依赖。另外,前期做领域梳理,可能还有一些业务前期不熟悉,后续会抽成单独领域服务的,比如促销优惠上下文,会抽象出促销领域服务
- 反复 1~3 过程,经历几个大型项目,结合业务多总结思考,会对 DDD 实际是解决业务划分的问题会感触更深
- DDD 反模式,上来直接先设计 DB 和数据库存储结构,再做业务建模
4. 人员请假领域 - 和业务专家/产品事件风暴后,尝试抽象领域行为表,让业务领域清晰起来
可以通过表格简要记录 DDD 各分层的领域对象名称,让依次做方法名的
5. 注意:不应该陷入 DDD 的术语、模式本身,应该通过其方法论,在业务拆分和代码分层方面的作用
- 统一术语: DDD 有许多概念(值对象、实体、聚合等),重要的部分不是模式本身,而是组织代码,使其与业务问题保持一致,并使用相同的业务术语(无处不在的领域语言,业务、产品、技术、测试…,账号绑定领域举例:融合绑定、切换绑定,大家能快速对齐是什么概念)。
- 领域边界: 在设计和定义微服务时,在哪里划定边界是关键任务。DDD 模式可帮助你了解领域中的复杂性。对于每个有界上下文的域模型,你可以识别和定义为域建模的实体、值对象和聚合。你构建和优化包含在定义你的上下文的边界内的域模型。这在微服务的形式中是明确的。这些边界内的组件最终成为你的微服务,尽管在某些情况下,BC 或业务微服务可以由多个物理服务组成。DDD 是关于边界的,微服务也是如此。
- 特别注意:仅当正在实施具有重要业务规则的复杂微服务时,才应应用 DDD 方法,更简单的职责,如 CRUD 服务,可以使用更简单的方法进行管理。
6. 分层:DDD 中技术核心一块,开发流程转变,从数据库 DB 设计转到从业务领域建模设计
6.1. 领域层(业务核心)
- 领域层是业务核心,按先分析,然后设计,最后才是开发,这样才能分析核心业务
- DDD 反模式,领域划分不清楚,导致领域内有多个上下文相关信息,代码必然会混乱
- 仓储接口(依赖接口编程,而非实现编程):
- 业务领域代码会有存储需求,通过依赖注入方式,解耦对基础设施层的依赖
- 实体、值对象、聚合、聚合根:
- 实体可以简单理解为领域的核心对象,有属性、操作方法(可以理解为 OOP 中的实体),比如登录领域(实体账号,有属性还有登录方法)、商品领域(商品实体)、订单领域(用户、购物车、商品、订单)
- 实体采用充血模型实现所有与之相关的业务,若单一实体或值对象无法实现领域中的功能,可以借助领域服务组合多个实体(或值对象)实现复杂的业务逻辑(比如下单服务,涉及用户、商品、订单、购物车实体交互)
- 值对象:
- 领域中的特殊对象,把一组相关属性组合在一起的对象,可以简单理解为一组属性的集合,比如用户收货地址,可以打包成一个值对象;
- 值对象可以嵌入到实体属性中,比如收货地址值对象嵌入到用户收货属性中
- 聚合(领域聚合服务):
- 在一个限定上下文中(在某个领域内),紧密相关的实体、值对象进行组合,就构成了聚合,有更强的表现
- 注意:如果是两个完全不同领域的信息聚合,应该通过应用服务来组合
- 聚合根(根实体):
- 聚合根也是称根实体,是聚合中的主负责人,作为聚合管理者,对外统一(比如商品领域包含其他评论实体,但统一选择用商品作为聚合根)
- 聚合根说白了就是在一组聚合中选出一个对外代表领域的根实体
6.2. 应用层(流程编排)
- 应用层做哪些内容:
- 做多个领域服务的聚合(课详内容:包含课程资料、课程类型、课程价格、课程销量,一定会存在业务聚合的情况,这块应用层做这个事情的)
- 做业务流程的编排处理,说明清楚业务 1234,但实际不应该有逻辑,逻辑在领域内(比如发布课程:课程资产入库、图片上传、老师人员、客服人员安排等)
- 事件通知、发布订阅、权限校验、安全认证等(比如上课提醒消息)
- 反模式:在应用层写业务逻辑代码,这样 Domain 层和应用层就职责不清,代码难维护
6.3. 接口层(参数处理、DTO)
- 做参数的基本处理,比如入参校验,回参 DTO 转换(拆包、组包)
6.4. 基础设施层(仓储接口实现,DB、Redis实际的读写操作)
基础层是基于依赖倒置设计的,封装基础资源服务,通过依赖注入方式解耦
基础组件都放这提供对其他层的支撑(包括第三方工具、MQ、文件、缓存、数据库、搜索等)
实现仓储接口 DB,通常真正读写 DB,实现仓储接口的语句都是写在这块
7. 分层:传统三层逻辑架构分层,转成 DDD 四层架构
三层在巨石单体架构,业务逻辑层通过模块划分,业务不复杂时候,可以较好解决当时问题;但当业务复杂后,业务逻辑包含流程处理、多实体交互作用,核心代码逻辑就开始变得臃肿和混乱。DDD 是通过将流程编排放到应用层,业务领域实现下沉为核心业务实现,可以让职责更清晰;
仓储接口和仓储实现:仓储接口放在领域层中,仓储实现放在基础层,原来三层架构通用的第三方工具包、驱动、Common、Utility、Config 等通用的公共的资源类统一放到了基础层。
8. 业务会发展,服务是会演进的,若微服务过大,是可以继续拆分的
当一个微服务过大,内部业务概念过于复杂,就会继续拆分以降低业务复杂度。前提是能做好领域设计,避免对业务理解不透彻,拆解得服务部合理,陷入微服务过多、微服务职责不清的困境
说明
- 服务 1 中聚合 a 的功能经常被高频访问,拆解服务 2
- 服务 3 的聚合 d 更适合微服 1 领域,从微服务 3 挪到微服 1
- 最终服务通过演进,服务职责和编辑会更加清晰
演进后效果,领域模型会更加精炼,而且服务会更内聚
9. 补充
9.1. 中台和 DDD 以及微服务的关系思考
DDD 完全是从业务视角出发剖析,中台可以从业务和技术视角出发(比如拆成业务中台和技术中台)
在中台中,通用业务,比如支付中台(包含支付、营销、订单等领域),这些在 DDD 中也可以有对应的支付、营销领域可对应
在中台中,技术中台,更多是偏通用可复用的技术,这些在 DDD 中可以理解成是支持域
9.2. UT 和 DDD 没有什么必然关系,但良好的拆包、函数抽离是可以让 UT 有很好的覆盖率
《Go 圣经》任何包系统设计的目的都是为了简化大型程序的设计和维护工作,通过将一组相关的特性放进一个独立的单元以便于理解和更新,在每个单元更新的同时保持和程序中其它单元的相对独立性。这种模块化的特性允许每个包可以被其它的不同项目共享和重用
UT 可以尝试:
- 将面条代码拆分成多个流程,每个流程每个环节都是一个方法或者函数,都是可以被测试的
- 尝试借用控制反转思想,基于依赖注入方式,结合面向接口编程思想,让你的代码写成可测试的
10. 小结
DDD本质是软件工程中的一种方法论,注意避免一些DDD的反模式,以业务领域为中心,遵循一定的流程,搞清楚各层的职责,通过接口解耦各层间的依赖,落地实践应下来就不会那么困难了
10.1. DDD 落地流程
关注业务领域,先划分业务边界,确定业务核心领域,最后才是在每个领域内通过 DDD 分层方式落地微服务
10.2. DDD 常见反模式
- 无论业务大小上来直接 DDD 建模,杀鸡用牛刀
- 上来直接 DB 表设计,而非业务领域建模设计
- 在应用层写业务逻辑代码,导致 Domain 层和应用层就职责不清,代码难维护
- 对基础设施层依赖,而非通过领域仓储接口将基础设施层解耦,导致应用、领域层和基础设施层耦合严重,分层职责不清,难做 UT
- 不断起新的微服务,没有关注原有业务领域的演进,导致服务越发难维护
10.3. 设计一个面向 DDD 的微服务,分层实践
10.4. 推荐 go 语言版本 DDD 的目录结构
|
|
Github地址: https://github.com/lupguo/ddd-layout
11. DDD 资源参考
- DDD 研习社: https://time.geekbang.org/qconplus/detail/100059794
- DDD 实战课专栏:https://time.geekbang.org/column/article/161004
- 如何落地业务建模: https://time.geekbang.org/column/intro/100082101
- Martinfolwer DDD: https://martinfowler.com/bliki/DomainDrivenDesign.html
- Microsoft .Net DDD: https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/ddd-oriented-microservice