DDD的Go实战

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: 看过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语言
相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。   相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情: https://www.aliyun.com/product/rds/mysql 
相关文章
|
1月前
|
数据采集 数据挖掘 测试技术
Go与Python爬虫实战对比:从开发效率到性能瓶颈的深度解析
本文对比了Python与Go在爬虫开发中的特点。Python凭借Scrapy等框架在开发效率和易用性上占优,适合快速开发与中小型项目;而Go凭借高并发和高性能优势,适用于大规模、长期运行的爬虫服务。文章通过代码示例和性能测试,分析了两者在并发能力、错误处理、部署维护等方面的差异,并探讨了未来融合发展的趋势。
150 0
|
1月前
|
存储 人工智能 Go
Go-Zero全流程实战即时通讯
Go-Zero 是一个功能丰富的微服务框架,适用于开发高性能的即时通讯应用。它具备中间件、工具库和代码生成器,简化开发流程。本文介绍其环境搭建、项目初始化及即时通讯功能实现,涵盖用户认证、消息收发和实时推送,帮助开发者快速上手。
184 0
|
5天前
|
存储 前端开发 JavaScript
Go语言实战案例-项目实战篇:编写一个轻量级在线聊天室
本文介绍如何用Go语言从零实现一个轻量级在线聊天室,基于WebSocket实现实时通信,支持多人消息广播。涵盖前后端开发、技术选型与功能扩展,助你掌握Go高并发与实时通信核心技术。
|
1月前
|
负载均衡 监控 Java
微服务稳定性三板斧:熔断、限流与负载均衡全面解析(附 Hystrix-Go 实战代码)
在微服务架构中,高可用与稳定性至关重要。本文详解熔断、限流与负载均衡三大关键技术,结合API网关与Hystrix-Go实战,帮助构建健壮、弹性的微服务系统。
237 1
微服务稳定性三板斧:熔断、限流与负载均衡全面解析(附 Hystrix-Go 实战代码)
|
1月前
|
安全 Go 开发者
Go语言实战案例:使用sync.Mutex实现资源加锁
在Go语言并发编程中,数据共享可能导致竞态条件,使用 `sync.Mutex` 可以有效避免这一问题。本文详细介绍了互斥锁的基本概念、加锁原理及实战应用,通过构建并发安全的计数器演示了加锁与未加锁的区别,并封装了一个线程安全的计数器结构。同时对比了Go中常见的同步机制,帮助开发者理解何时应使用 `Mutex` 及其注意事项。掌握 `Mutex` 是实现高效、安全并发编程的重要基础。
|
1月前
|
数据采集 Go API
Go语言实战案例:使用context控制协程取消
本文详解 Go 语言中 `context` 包的使用,通过实际案例演示如何利用 `context` 控制协程的生命周期,实现任务取消、超时控制及优雅退出,提升并发程序的稳定性与资源管理能力。
|
1月前
|
数据采集 Go API
Go语言实战案例:多协程并发下载网页内容
本文是《Go语言100个实战案例 · 网络与并发篇》第6篇,讲解如何使用 Goroutine 和 Channel 实现多协程并发抓取网页内容,提升网络请求效率。通过实战掌握高并发编程技巧,构建爬虫、内容聚合器等工具,涵盖 WaitGroup、超时控制、错误处理等核心知识点。
|
1月前
|
Go 开发者
Go语言实战案例:使用select监听多个channel
本文为《Go语言100个实战案例 · 网络与并发篇》第5篇,详解Go并发核心工具`select`的使用。通过实际案例讲解如何监听多个Channel、实现多任务处理、超时控制和非阻塞通信,帮助开发者掌握Go并发编程中的多路异步事件处理技巧。
|
1月前
|
数据采集 编解码 监控
Go语言实战案例:使用channel实现生产者消费者模型
本文是「Go语言100个实战案例 · 网络与并发篇」第4篇,通过实战案例详解使用 Channel 实现生产者-消费者模型,涵盖并发控制、任务调度及Go语言并发哲学,助你掌握优雅的并发编程技巧。
|
1月前
|
数据采集 消息中间件 编解码
Go语言实战案例:使用 Goroutine 并发打印
本文通过简单案例讲解 Go 语言核心并发模型 Goroutine,涵盖协程启动、输出控制、主程序退出机制,并结合 sync.WaitGroup 实现并发任务同步,帮助理解 Go 并发设计思想与实际应用。