1. 前言
最近一直忙着总结复习,在重阅王启军老师的《持续演进的云原生架构》,看到微服务拆分时候提到了 DDD,想起前不久同事也问到 DDD 相关问题,加上自己在写一些小项目时候,也是复用的 DDD 布局架构,想着还是再系统总结下 DDD 落地实践经验。
1.1. DDD 代码布局的几个小项目
- wisdom-httpd: 嵌入 Notion 模版内的“智者语言”
- copilot_develop: 用于 Blog 的 AI 摘要生成器
1.2. 过往有关 DDD 文章
以前陆续写过几篇 DDD 的文章,有一些零散,这篇文章算是对之前的 Blog 内容,加上过往项目经验的综合总结,可以看下之前的文章:
- 开发 Go 服务有关 DDD 分层架构思考与实践: https://ln/ddd/go-ddd-sample-2020
- 项目实践 DDD 后的一些思考: https://tkstorm.com/ln/ddd/ddd-think-2021
- DDD 脚手架工具: https://tkstorm.com/ln/ddd/ddd-layout-tools-2023
1.3. Go 微服务 DDD 代码布局供参考
花了一些时间把之前的go-ddd-layout
github 代码仓库完善了下,最新布局参考: https://github.com/lupguo/go-ddd-layout
2. DDD 理解
DDD 是一种复杂业务(战略设计、战术实施)软件架构方法,通过分治思想将大型复杂业务完成业务域划分、领域建模确定,指导微服务落地,以便更好地管理业务复杂性并提高代码可维护性
3. DDD 具体步骤
DDD 具体步骤包含: DDD-战略设计、DDD-战术实施两部份,战略设计聚焦在业务分治,通过梳理业务,领域划分,完成领域建模,战略实施聚焦在领域概念代码映射,完成微服务的编码落地
3.1. DDD - 战略设计(梳理业务,领域划分,完成领域建模)
- 业务梳理(事件风暴、领域故事分析):搞清楚业务流、用例,深入理解业务需求流程和规则
- 领域划分(业务域、子域划分):基于业务流、用例划分业务域、子域,区分哪些是核心域、支持域
- 领域建模(领域对象梳理):明确领域内核心要素,提炼业务域内的实体、值对象、聚合、聚合根
- 服务提炼(划定限定文边界):基于业务域能力内聚原则,根据不同子领域之间的关系和依赖性,明确领域边界,提炼出领域下的服务;基于聚合根定义服务接口(RPC),确保单一职责,清晰且有意义,如果存在模糊,重复步骤 2、3
3.2. DDD - 战术实施(将领域概念映射成具体的服务代码)
战术实施即服务开发,旨在将领域核心映射成服务代码
Go 微服务 DDD 代码布局参考: https://github.com/lupguo/go-ddd-layout
服务代码 DDD 目录分层推荐(接口层、应用层、领域层、基础设施层),每层职责和边界清晰,可以具体团队规模、业务复杂实际情况,控制服务的内聚程度,避免一开始服务拆分粒度过细导致服务数量增长过快,增大服务维护和治理成本。
- 接口层:协议转换(DTO)、参数校验、错误处理
- 应用层:业务流程编排(不含实际处理逻辑),针对业务缓存、分布式锁、事件通知、任务调度非业务流操作也放应用层(不含实现内容)
- 领域层:领域服务核心能力,提供业务逻辑实现(比如赤字数据生成读取、报表下载、GMV 等),核心要素包括领域服务
service
、领域实体entity
、仓储接口repository
- 基础设施层:实现仓储接口,RPC 具体实现、DB 存储、缓存、消息中间件等,都会去实现领域层定义好的仓储接口
4. Go 微服务 DDD 代码布局
4.1. Go 微服务 DDD 布局实践
以下是在之前腾讯课堂研发团队 Go 微服务项目开发过程中使用的 DDD 布局参考,相关布局规范也通过标准化专项推广到了全组中。
我将下述布局抽象到 Go 微服务 DDD 代码布局供参考: https://github.com/lupguo/go-ddd-layout,其他团队在实践落地过程可以适当灵活调整,形成自己的统一标准即可!
4.2. 项目布局代码示例参考
一些个人项目代码布局示例参考:
以前调研过程中 DDD 布局借鉴:
5. 服务分层代码实施细节示例说明
5.1. 接口层(协议转换、参数检测):
接口层职责:做参数的基本处理,比如入参校验,回参 DTO 转换,主要职责包括拆包、组包,RPC、HTTP 协议转换成内部应用层可以处理的对象
接口层简单将req
参数转换成entity.UploadInfo
值对象,处理完后将结果回填到rsp
响应中
|
|
5.2. 应用层(流程编排、缓存、事件通知)
应用层职责:做领域下一个或多个领域服务的业务流程编排,对外提供聚合能力,说明清楚业务 1234 步骤
例如下单应用会调用订单服务的订单创建,同时应用层会做密等性设计、库存检测、订单创建数据获取、订单创建、库存扣减、支付通知等
补充说明
- 应用层负责业务流编排不写业务逻辑,业务逻辑应该内聚在领域内实现
- 应该编排不同领域服务的逻辑,不要下沉到领域层,下沉后**容易导致领域层的服务耦合变高,领域服务职责不单一,变得复杂(**参考 应用层的
UploadImage
实现) - 事件通知、缓存处理,应该在应用层实现
- 如果业务很简单,应用层可做简单透传
- 参考图片上传包含两个步骤,上传、存储两者实际是解耦开来的,后续扩展仅需要扩展一种上传类型
from
,并在 Service 内针对性的扩展一个方法,其他服务无需变动 - 图片上传处理,基于类型完成图片上传、水印、存储
- 将处理后的图片存储到 DB
|
|
5.3. 领域层(子域下服务核心)
领域层职责: 领域服务核心能力,提供业务逻辑实现(比如赤字数据生成读取、报表下载、GMV 等),核心要素包括领域服务
service
、领域实体entity
、仓储接口repository
5.3.1. 领域服务(优先设计)服务领域接口设计和实现,参考 IServiceImageUpload
接口
|
|
5.3.2. 实体(OOP 实体)、值对象
- 实体可以简单理解为领域的核心对象,比如登录领域(账号)、商品领域(商品、商品价格、商品库存、商品类目、商品规格等)、订单领域(订单、订单用户、订单购物车、订单商品)
- 实体采用充血模型实现所有与之相关的业务,若单一实体或值对象无法实现领域中的功能,可以借助领域服务组合多个实体(或值对象)实现复杂的业务逻辑(比如文件上传聚合信息、订单收货地址等),例如在下单上下文中,领域内商品订单(实体),收货地址(可以是值对象)
|
|
5.3.3. 仓储接口(基于依赖设计)
- **依赖接口编程,而非实现编程,这样可以解耦对基础设施的实现依赖。**业务领域代码的存储、RPC、消息队列、外部 HTTP 请求等,都可以通过依赖注入方式,解耦对基础设施层的依赖
|
|
5.4. 基础设施层(实现仓储接口)
- 基础层是基于依赖倒置设计的,封装基础服务能力,常见有三方工具、MQ、文件、缓存、数据库、搜索等,通过依赖注入方式解耦
- 例如
MysqlInfra
实现了IReposStorage
仓储接口,负责图片对存储、查询功能:
|
|
6. DDD 反模式
6.1. 战略设计过程中反模式行为
- 业务梳理投入不足,缺乏对业务的深入和系统的理解,导致后续领域建模、限定上下文划分不合理
- 业务过度设计,还没开始就把业务按百万日活访问,过度拆分导致服务数量增大,服务治理维护难度增大
- 限定上下文划分不合理,导致后续服务的职责边界模糊,带来设计不合理,引入服务间 RPC 通信的开销
6.2. 战术实施过程中反模式行为
没有严格遵循各层职责,常见在应用层写业务逻辑代码,各层职责不清导致业务逻辑割裂代码难维护
过度抽象导致代码变得复杂且难以理解,同时还增加了维护成本
过度依赖 ORM 框架可能会破坏领域对象之间的聚合关系,并导致性能问题
过度关注技术细节,DDD 鼓励开发人员专注于业务需求,而不是过度关注技术实现细节,过度关注技术细节,可能会导致失去对业务需求的理解,并且无法满足真正的业务价值
DDD 资源参考
- DDD 学习
- DDD 架构分层:https://time.geekbang.org/column/article/156849
- DDD 中台和微服务:https://time.geekbang.org/column/article/161004
- DDD 研习社: https://time.geekbang.org/qconplus/detail/100059794
- Design a DDD-oriented microservice:https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/ddd-oriented-microservice
- https://mariocarrion.com/2021/03/21/golang-microservices-domain-driven-design-project-layout.html