项目实践DDD后的一些思考

AI 摘要: 本文简要介绍了领域驱动设计(DDD)在实践中的应用,并分享了对DDD的新认知。通过解析领域边界和分层设计,将大型业务领域拆分成多个子领域,并将其落地为微服务。同时强调了统一术语和注意事项,并提供了DDD落地流程和常见反模式的解决方法。最后,推荐了一个适用于Go语言的DDD目录结构模板,并提供了相关资源参考。

最近在课堂账号项目中用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/

  1. 先画 BPMN(业务流图)、UML Activity(实体活动图),与测试用例结合(可以和业务专家事件风暴,深入业务讨论),先把业务探讨清楚,比如一个用户下单场景:
    • 商品加购物车上下文
      1. 用户添加商品购物车、删除购物、清空购物车(实体有用户、商品、购物车)
      2. 用户添加商品,增加商品、减少商品数量(实体有用户、商品)
    • 订单结算上下文
      1. 用户虚拟信息相关(比如课点、优惠券)
      2. 优惠计算
      3. 订单风控
      4. 订单生单
    • 订单支付上下文
      1. 虚拟资产扣减(课点、优惠券)
      2. 用户选择支付方式(实体有用户、支付方式)
  2. 通过步骤 1 对业务了解后:
    • 发现有 3 个领域的上下文:购物车操作、订单结算、订单支付
    • 拆解成 3 个领域服务跟进,对应微服务就是购物车、订单、支付 3 个微服务
  3. 步骤 1 实际有发现领域内还有用户、商品等信息,表明购物车实际也是对其他领域服务有依赖。另外,前期做领域梳理,可能还有一些业务前期不熟悉,后续会抽成单独领域服务的,比如促销优惠上下文,会抽象出促销领域服务
  4. 反复 1~3 过程,经历几个大型项目,结合业务多总结思考,会对 DDD 实际是解决业务划分的问题会感触更深
  5. DDD 反模式,上来直接先设计 DB 和数据库存储结构,再做业务建模

4. 人员请假领域 - 和业务专家/产品事件风暴后,尝试抽象领域行为表,让业务领域清晰起来

可以通过表格简要记录 DDD 各分层的领域对象名称,让依次做方法名的

5. 注意:不应该陷入 DDD 的术语、模式本身,应该通过其方法论,在业务拆分和代码分层方面的作用

https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/ddd-oriented-microservice

  1. 统一术语: DDD 有许多概念(值对象、实体、聚合等),重要的部分不是模式本身,而是组织代码,使其与业务问题保持一致,并使用相同的业务术语(无处不在的领域语言,业务、产品、技术、测试…,账号绑定领域举例:融合绑定、切换绑定,大家能快速对齐是什么概念)。
  2. 领域边界: 在设计和定义微服务时,在哪里划定边界是关键任务。DDD 模式可帮助你了解领域中的复杂性。对于每个有界上下文的域模型,你可以识别和定义为域建模的实体、值对象和聚合。你构建和优化包含在定义你的上下文的边界内的域模型。这在微服务的形式中是明确的。这些边界内的组件最终成为你的微服务,尽管在某些情况下,BC 或业务微服务可以由多个物理服务组成。DDD 是关于边界的,微服务也是如此。
  3. 特别注意:仅当正在实施具有重要业务规则的复杂微服务时,才应应用 DDD 方法,更简单的职责,如 CRUD 服务,可以使用更简单的方法进行管理。

6. 分层:DDD 中技术核心一块,开发流程转变,从数据库 DB 设计转到从业务领域建模设计

https://time.geekbang.org/column/article/156849

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 以及微服务的关系思考

  1. 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 的微服务,分层实践

https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/ddd-oriented-microservice

10.4. 推荐 go 语言版本 DDD 的目录结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
$ tree -L 3
├── Makefile
├── README.md
├── app
│    ├── application    # 应用层,内部文件以`_app.go`结尾
│    │    ├── image_search_app.go
│    │    └── image_upload_app.go
│    ├── domain         # 领域层,依赖仓储接口
│    │    ├── entity        # 领域实体,以`_entity.go`结尾
│    │    ├── repository    # 仓储接口,以`_repos.go`结尾
│    │    ├── service       # 领域服务,以`_service.go`结尾
│    │    └── valobj        # 值对象,以`_valobj.go`结尾
│    ├── infrastructure # 基础上设施层,实现仓储接口
│    │    ├── dbs
│    │    ├── esearch
│    │    ├── mqs
│    │    └── rds
│    └── interfaces     # 接口层,依赖application层
│        ├── search_intf.go     # 接口实现,后缀可以`_intf.go`结尾
│        ├── tag_intf.go
│        └── upload_intf.go
├── build               # 多操作系统,编译文件生成
├── cmd
│    └── myapp          # 目录文件夹为应用名称,期间仅包含一个main.go文件
├── configs             # 服务配置文件,包括配置中心、错误码等信息
│    ├── apollo
│    ├── confd
│    └── errcode
├── deployments         # CI/CD持续部署相关的一些脚本
├── docs                # 项目相关文档
├── go.mod
├── internel            # 从项目内抽离的包,可能被复用到其他项目中去的包
│    └── reusable
└── test
    └── testdata

Github地址: https://github.com/lupguo/ddd-layout

11. DDD 资源参考

  1. DDD 研习社: https://time.geekbang.org/qconplus/detail/100059794
  2. DDD 实战课专栏:https://time.geekbang.org/column/article/161004
  3. 如何落地业务建模: https://time.geekbang.org/column/intro/100082101
  4. Martinfolwer DDD: https://martinfowler.com/bliki/DomainDrivenDesign.html
  5. Microsoft .Net DDD: https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/ddd-oriented-microservice