DDD的Go实战

看过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语言

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值