DDD的Go实战

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 看过DDD的一些书,这次将自己的理解转化为代码。论语里说“学而不思则罔,思而不学则殆”,学会某种能力需要了解到新的知识并思考这些知识,比较好的方式便是动手实践。

看过DDD的一些书,这次将自己的理解转化为代码。论语里说“学而不思则罔,思而不学则殆”,学会某种能力需要了解到新的知识并思考这些知识,比较好的方式便是动手实践。

对DDD的资料,推荐如下:

  1. Eric Evans的《领域驱动设计——软件核心复杂性应对之道》或领域驱动设计精简版
  2. 沃恩·弗农的《实现领域驱动设计》
  3. DDD案例实战课

前两个都偏理论,后一个偏实战。如果大家没有足够的时间,可以看一下领域驱动设计读书笔记,里面包含了几乎所有的名词解释和作用说明。实战部分,可以看现在的这篇文章。

DDD以面向领域的之名实现面向对象的之实。

一、场景

1.1起因

做跨境业务的时候,负责过商家仓模块。这个模块功能相对简单,但后期发现代码开发、维护的越来越差,这也是为什么我想引入DDD的原因。DDD有一个重要作用,要求开发人员把业务对象想清楚再开发,并且设置了达成标准,即领域模型对象。

领域对象(Domain Object):包含领域模型对象(Domain Model Object)、资源库(Repository)、领域事件(Domain Event)以及应用服务所涉及到的命令(Command)和查询(Query)对象

领域模型对象:分为聚合、实体和值对象这三大类

1.2通用语言

商家仓是在真实的服务商仓上虚拟出的概念,一个服务商仓可以对应多个商家仓,目的是能够以更细的仓维度对商品进行管理。

商家仓场景:

  • 创建商家仓:商家可以创建商家仓,创建的商家仓需要对应一个服务商仓,商家仓的唯一id(warehouseid)需要从仓管理服务申请
  • 更新商家仓:商家仓创建后需运营审核通过才能使用
  • 查询商家仓:商家运营都能查看商家仓的信息,信息里需要包含服务商仓的内容

当然商家仓的场景还有很多,为了方便,我们只选择创建商家仓、更新商家仓状态、查询商家仓这几个场景。

DDD的具体实现没有标准的方案,本次实现只是其中一种,欢迎和大家一起讨论。

二、非DDD实现方案

虽然在Go中我们使用了对象,但本质上还是以面向过程的思维在进行开发。先演示一下常用的开发流程,以便与DDD方案比较。

2.1目录结构

一般而言,非DDD的目录结构如下:

.
├── dal
│   └── db //操作mysql
│       ├── init.go
│       ├── shopwarehouse.go
│       └── spwarehouse.go
├── handler   //入口
│   ├── createwarehouse.go
│   └── getwarehouse.go
├── idl
│   └── idl.go
├── model  //表结构
│   └── warehouse.go
└── service //通用业务逻辑
    └── warehouse.go

比较重要的几个目录为:

handler:入口函数,一般一个场景对应其中的一个函数

service:公共的业务逻辑可以放到service层

dal:实现存储的初始化、具体操作等,如初始化mysql,对mysql进行增删改查

model:与存储相关的数据结构,如mysql的表结构等放在该层

2.2开发过程

2.2.1代码

代码:https://github.com/shidawuhen/asap/tree/master/controller/warehouse/normal

核心逻辑在service层,service层管理业务逻辑、调用第三方服务、和数据库交互、数据组装、处理返回信息等。

/**
 * @Author: Jason Pang
 * @Description: 创建商家仓
 * @receiver s
 */func (s *shopWarehouseService) CreateShopWareHouse() bool {    //1.从第三方获取id rpc.getwarehouseid
    //2.从服务商仓获取信息 mysql
    //3.组装信息
    shopWarehouseInfo := model.ShopWareHouse{
        Id:            2,
        WarehouseId:   11,
        Code:          "商家仓2",
        Name:          "商家仓2",
        SpWareHouseId: 2,
        Status:        0, //init
    }    //4.插入数据库 mysql
    shopWareHouseRepo := db.DefaultShopWareHouseRepo()  //5.返回
    return shopWareHouseRepo.CreateShopWareHouse(&shopWarehouseInfo)
}

2.2.2编写过程

虽然我们使用了类,但Service类其实是个贫血模型,而且我们没有办法控制开发人员使用面向对象方案进行开发。这个目录结构对面向过程是天然适应的,只需要按照流程编写即可,从handler->service->dal。这种结构有很大的市场,符合人类的分析模式,学习、上手成本低。

但随着业务变的更加复杂、项目存活时间越长,代码会越来越乱、越来越难以管理。

关键问题在于service层包含太多功能,没有进行更细维度的拆分。

三、DDD实现方案

DDD的方案,强制让开发人员在码代码前,对业务进行深入的思考。因为使用DDD,需分析出有哪些对象,这些对象有哪些特性、能力,这些对象之间是如何交互的。一旦把这些事情想明白,能更从容的面对未来业务的变化。

3.1目录结构

.
├── app //serveice层
│   ├── commandservice //命令类service层
│   │   └── shopwarehouse_commandservice.go
│   └── queryservice //查询类service层
│       └── shopwarehouse_queryservice.go
├── controller //入口层
│   ├── assembler //将请求dto转化为command
│   │   └── shopwarehousecommand_assembler.go
│   ├── dto //请求的结构体
│   │   └── shopwarehouse_dto.go
│   └── shopwarehouse_controller.go //入口controller
├── domain //领域层
│   ├── command //命令
│   │   ├── shopwarehousecreate_command.go
│   │   └── shopwarehouseupdatestatus_command.go
│   ├── model //领域模型对象
│   │   ├── aggregate //聚合根
│   │   │   └── shopwarehouse.go
│   │   ├── entity //实体
│   │   │   └── spwarehouse.go
│   │   └── valueobject //值对象
│   │       ├── shopwarehousestatus.go
│   │       └── warehouseid.go
│   └── repo //资源库接口
│       └── repo.go
├── infra
│   └── persistence
│       ├── convertor //po和领域模型对象转换
│       │   └── warehouse_convert.go
│       ├── dal //真正操作db
│       │   ├── shopwarehouse_dal.go
│       │   └── spwarehouse_dal.go
│       ├── po //数据库表结构定义
│       │   ├── shopwarehouse_po.go
│       │   └── spwarehouse_po.go
│       ├── shopwarehouse_repo_impl.go //资源库接口的位置
│       └── spwarehouse_reop_impl.go
└── integration
    └── acl //防腐层,用于调用第三方,返回领域模型对象
        └── warehouse_acl.go 

通过该目录结构和说明,大家能够对DDD有个大概的认知,DDD中的限界上下文正好包含这几部分:

图片

3.2对应关系

下方是非DDD实现方案和DDD实现方案的对比,能够很明显的表现出两者之间的区别。

我们可以发现service层的功能被拆分了:

  • application层分为命令服务、查询服务,负责整个逻辑的编排,和service层的对应性最高
  • 项目的核心业务逻辑(领域)从以前杂糅在service层中,拆分到domain层,这一层也是最关键、最重要的一层,包含了这个项目最核心的信息
  • 数据组装、转换工作拆分到ACL、基础设施层

图片

图片链接:https://www.processon.com/view/link/62dbdf8907912953fdda6179

3.3开发过程

3.3.1代码

代码:https://github.com/shidawuhen/asap/tree/master/controller/warehouse/ddd

我们看一下application层和domain层的代码样例:

Application: 更新商家仓状态。主要负责逻辑编排,调用聚合实现服务功能。

//update等func (s *ShopWarehouseApplicationService) UpdateStatus(command *command.ShopWarehouseUpdateStatusCommand) error {    //1.从数据库获取商家仓信息
    shopWareInfo, _ := s.ShopWarehouseRepo.Find(s.ctx, command.WarehouseId.Get())    //2.调用聚合更新状态
    shopWarehouseAggregate := aggregate.ShopWarehouse{}
    shopWarehouse := shopWarehouseAggregate.UpdateStatus(command, shopWareInfo)    if shopWarehouse == nil {        return errors.New("更新失败")
    }    //3.存储
    s.ShopWarehouseRepo.Save(s.ctx, shopWarehouse)    return nil}

Domain:更新商家仓状态。虽然样例写的比较简单,但状态机等核心逻辑,全部坐落在Domain了。

func (s *ShopWarehouse) UpdateStatus(command *command.ShopWarehouseUpdateStatusCommand, shopWare *ShopWarehouse) *ShopWarehouse {    //此处是核心逻辑,判断更新的标准
    if shopWare.Status != command.Status {        return nil
    }
    shopWare.Status = command.Status    return shopWare
}

3.3.2编写过程

写DDD可按如下步骤进行

  1. 分析通用语言,找出命令和对应的领域模型对象(实体、聚合、值对象)
  2. 分析聚合所包含的核心业务逻辑,以及实现这些逻辑需要有哪些数据
  3. application层准备这些数据,需要确定repo的接口、ACL里需要调用哪些服务
  4. 在基础设施层真正实现repo接口,在ACL里真正调用这些服务
  5. controller层将请求参数转换为命令(command),然后调用application层的接口,完成命令的执行
3.3.2.1分析通用语言,找出领域模型对象

在第一节中我们用通用语言描述了商家仓的场景,我们尝试分析对应的领域模型对象

角色 命令 领域模型对象
商家 创建商家仓 商家仓、服务商仓
商家 查询商家仓 商家仓、服务商仓
运营 更新商家仓状态 商家仓

我们能够分析出三个命令,一个是查询类的,两个是执行类的,所以在domain/command下至少要建两个执行类的command。

我们分析出两个对象:服务商仓和商家仓,这两个都是实体(entity),但在当前场景下,商家仓也是聚合根,因为它本质上是包含服务商仓的。商家仓的warehouseid可以设置为值对象,因为这个数据不可变更。

3.3.2.2领域模型对象分析

领域模型对象包含聚合根、实体、值对象,这三者有如下区别:

  1. 实体:包含数据,可对数据进行赋值
  2. 值对象:包含数据,只能new,不可更改其值
  3. 聚合根:聚合根也是实体,但包含其它实体和值对象,并有对应的业务逻辑。在聚合根里,不能和其它系统(DB、RPC等)有交互。

商家仓聚合根包含两个业务逻辑-创建、更新状态,需要的数据如下:

创建:商家填写的商家仓部分信息、服务商仓信息

更新:商家仓warehouseid、要更新的状态、当前商家仓信息

通过分析,我们能初步写出domain下的创建和更新command、商家仓聚合根aggregate、服务商仓entity、warehouseid的valueobject。

3.3.2.3application层准备数据

application承担编排工作,调用聚合根完成核心功能,将结果进行存储。

我们知道命令有两类,分别是查询和执行类,所以在app层我们创建两个service。

对于创建和更新状态功能需要和DB、RPC交互,我们需要在

  1. repo层写接口,persistence层的impl写实现。persistence需要定义出表结构(po)、db的操作(dal),实现po与领域模型对象的转换(convertor)
  2. 在acl中实现与其它服务的交互,并返回指定领域模型对象

对于查询功能,一般只需要和DB交互,无需关注领域模型,直接调用repo即可。

通过分析,我们能初步写出app中的服务,domain的repo接口,infra下的impl、po、dal、convertor,integration下的acl。

3.3.2.4controller接收请求

无论是query还是command类的请求,都由controller承接,controller需要将请求参数转换为指定命令结构,然后调用相关的服务完成操作,并将操作结构返回。

通过分析,我们能初步写出controller下的入口函数、dto、assembler。

在controller/base/base.go中我们实现对controller的调用,大家可以执行一下,看一下返回结果。

四、总结

至此聊完了整个实战过程,大家一边看代码一边看编写过程能更容易理解。

DDD里有很多细节在本文中没有提及,如为了保证对领域模型对象的操作符合规范需要怎么设计、如果要产生事件需怎么做等。之所以不写这些还是希望大家能够先了解整体框架,然后在这个基础上不断补充细节,这样接受起来会更加简单。

完全按照DDD来实现,能够达成以领域模型对象限制整个代码的目标,使开发变得更加规范,代价是开发的难度、复杂度提升。具体怎么设计,丰俭由人,但找出领域模型对象是必须的,因为这部分能够表示开发人员真的深入思考过业务。

五、名词解释

DDD有一些常用名词,此处进行整理,方便大家查询。

  1. ACL:防腐层(Anticorruption Layer,ACL)
  2. UP:统一协议(Unified Protocol,UP)
  3. EDA:事件驱动架构(Event-Driven Architecture,EDA)
  4. CQRS:的全称是 Command Query Responsibility Segregation,也就是命令和查询职责分离
  5. CRUD:增加(Create)、检索(Retrieve)、更新(Update)和删除(Delete)
  6. DMO:领域模型对象(Domain Model Object)
  7. AOP:切面
  8. DMO:领域模型对象(Domain Model Object),聚合根、实体、值对象
  9. DO:领域对象(Domain Object),包含领域模型对象(Domain Model Object)、资源库(Repository)、领域事件(Domain Event)以及应用服务所涉及到的命令(Command)和查询(Query)对象
  10. PO:资源库实现部分所对应的数据对象称为是一种持久化对象(Persistence Object,PO)
  11. DM:数据映射器(Data Mapper) 的概念。映射(Mapping)思想在软件设计过程中非常常用,主要用于分离不同层次之间的数据耦合。对于数据访问而言,数据映射器的作用在于分离领域对象和持久化媒介
  12. VO(View Object)视图对象:和视图打交道的,那么经历了视图的都归属于这个类,所以我们的输入输出类都是属于VO
  13. DTO(Data Transfer Object)**数据传输对象:**我们sql查询的时候是通过Id查询的,但是查询是可以查询出很多条信息的,但是我们给前端的数据只要某一部分,比如上例有4个属性,但是只要求输出3个。
  14. DAO:Data Access Object,数据访问对象1.用来封装对数据库的访问(CRUD)2.通过接收Business层的数据,将POJO持久化为PO
  15. BO( Business Object):业务对象。由Service层输出的封装业务逻辑的对象。

资料

  1. https://tech.bytedance.net/articles/7103447005514924045
  2. https://tech.bytedance.net/articles/6963521013434810404
  3. processon https://www.processon.com/view/link/62dbdf8907912953fdda6179
  4. 实战 https://juejin.cn/book/7056372655913435172/section/7062144186283196424
  5. 切面 https://zhuanlan.zhihu.com/p/421999882
  6. https://github.com/tianminzheng/customer-service
  7. https://github.com/tianminzheng/customer-service-axon

最后

大家如果喜欢我的文章,可以关注我的公众号(程序员麻辣烫)

我的个人博客为:https://shidawuhen.github.io/

往期文章回顾:

  1. 设计模式
  2. 招聘
  3. 思考
  4. 存储
  5. 算法系列
  6. 读书笔记
  7. 小工具
  8. 架构
  9. 网络
  10. Go语言
相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
3月前
|
消息中间件 人工智能 供应链
go-zero 微服务实战系列(二、服务拆分)
go-zero 微服务实战系列(二、服务拆分)
|
2月前
|
Shell Go API
Go语言grequests库并发请求的实战案例
Go语言grequests库并发请求的实战案例
|
5月前
|
存储 算法 Go
go语言并发实战——日志收集系统(七) etcd的介绍与简单使用
go语言并发实战——日志收集系统(七) etcd的介绍与简单使用
|
2月前
|
安全 大数据 Go
深入探索Go语言并发编程:Goroutines与Channels的实战应用
在当今高性能、高并发的应用需求下,Go语言以其独特的并发模型——Goroutines和Channels,成为了众多开发者眼中的璀璨明星。本文不仅阐述了Goroutines作为轻量级线程的优势,还深入剖析了Channels作为Goroutines间通信的桥梁,如何优雅地解决并发编程中的复杂问题。通过实战案例,我们将展示如何利用这些特性构建高效、可扩展的并发系统,同时探讨并发编程中常见的陷阱与最佳实践,为读者打开Go语言并发编程的广阔视野。
|
3月前
|
消息中间件 缓存 Kafka
go-zero微服务实战系列(八、如何处理每秒上万次的下单请求)
go-zero微服务实战系列(八、如何处理每秒上万次的下单请求)
|
3月前
|
缓存 NoSQL Redis
go-zero微服务实战系列(七、请求量这么高该如何优化)
go-zero微服务实战系列(七、请求量这么高该如何优化)
|
3月前
|
缓存 NoSQL 数据库
go-zero微服务实战系列(五、缓存代码怎么写)
go-zero微服务实战系列(五、缓存代码怎么写)
|
3月前
|
API
企业项目迁移go-zero实战(二)
企业项目迁移go-zero实战(二)
|
3月前
|
消息中间件 存储 NoSQL
redis实战——go-redis的使用与redis基础数据类型的使用场景(一)
本文档介绍了如何使用 Go 语言中的 `go-redis` 库操作 Redis 数据库
181 0
redis实战——go-redis的使用与redis基础数据类型的使用场景(一)
|
3月前
|
消息中间件 SQL 关系型数据库
go-zero微服务实战系列(十、分布式事务如何实现)
go-zero微服务实战系列(十、分布式事务如何实现)