/go-clean-arch

Go Clean Architecture

Primary LanguageGo

整洁架构 ( Go )

主要围绕如下几点展开讨论,

  1. 代码规范
    1. 分层思路
    2. 目录结构
    3. 数据分层
  2. 目录结构
    1. 每一层拆分后,水平是否需要在进行分层
    2. 公共的功能放在什么地方
  3. 工程化规范
    1. errors 统一处理规范
    2. API 接口的规范
  4. 其他
    1. 如何保障测试
    2. 错误日志太多:规范日志的输出
    3. 告警太多,导致几乎没人再去关注告警



架构**

工程中大量借鉴了 The Clean ArchitectureDDD 提出的**,所以先对这 2 个架构的基本思路进行介绍。

整洁架构的主要**可以参考 The Clean Architecture 这篇文章,DDD 可以参考 阿里技术专家详解 DDD 系列 文章。

整洁架构的好处

引入整洁架构带来的好处主要包括:

  1. 独立于框架:不应该依赖于某种框架,也不需要让系统适应框架。
  2. 可被测试性:可以脱离各种依赖进行测试,比如:UI、数据库、Web 服务等外部因素。
  3. 独立的 UI:对 UI 进行解耦合,不依赖于具体的 UI 实现,也较为容易的可以切换 UI,比如从 Web 界面切换到命令行界面。
  4. 独立于数据库:业务逻辑与具体的数据库解耦,不依赖于具体的数据库,不关心是 MySQL、Oracle 或者其他任何类型的数据库。
  5. 独立于任何外部的依赖:业务逻辑不需要关心任何的外部接口。

整洁架构的模型

上图描述了整洁架构的一个基本模型示例,介绍一下文中的基本概念:

  1. Entities:即实体,类似于 DDD 中的 Domain 层的概念,里面包含对应的业务逻辑
  2. Use Cases:用例,类似于 DDD 中的 Application Service,主要包含各种业务逻辑的编排
  3. 各类依赖和数据渲染层在外层,不会对内部的业务逻辑规则产生影响。
  4. 最重要的是:在图中的依赖关系,内部圈不依赖外层圈
  5. 虽然实际的调用关系是从外层一层一层的调用到最内部的业务逻辑,但是是依靠 依赖注入控制反转 方式进行解耦。



代码规范

代码的目录结构参考了 github.com/go-kit/kitgithub.com/go-kratos/kratosgithub.com/golang-standards/project-layout 等工程的代码结构**,提出下列规范:

从根目录,开始分为:

/api

职责

  • 定义接口协议 DTO 结构体。可以引入 protobuf 来定义协议结构体。
  • 路由注册
  • 请求/响应序列化和反序列化

如何测试

  • 使用 httptest 模拟 http server , 然后请求接口。

测试关注点

  • HTTP 请求和响应的序列化是否符合预期

/cmd

职责

  • main.gofunc main()
  • func main 函数中,做服务的初始化、依赖注入。这里会有两个问题:

/internal

强制增加 /internal package,防止其他工程随意引用。

  • 可以避免循环引用的问题。
  • 规范调用关系,如果不是我们自己服务的调用的话,那么就应该使用 rpc 的调用方式。

在这下面可以创建子目录,

子目录 职责 备注
/internal/server HTTP Server, gRPC Server 的定义。在这里面主要是对 Server 的生命周期进行管理,这也是很多微服务框架的主要工作之一。比如,对 HTTP Server 的优雅退出进行管理。
1、创建 HTTP Server,管理 HTTP Server 的生命周期,包括优雅退出的策略。2、 ( 重点 ) ( 类似于 gRPC ) 使用 Register 的方式将 Server 注入到 /api 中,绑定 Server 与 http router 的关系。
/internal/service 1、重点:参数转换,并做简单的参数校验。 2、做业务数据的渲染。 ( 由于没有 BFF,所以将 BFF 的功能放到这一层做,但是会导致这一层的代码膨胀 ) service层 --> usecase层 中的 Usercase。
/internal/domain 保存 domain 级别的对象,其中包含:domain objectvalue objectdomain service 。 按照 DDD 中的**,Domain Object 里面包含各自负责领域的业务逻辑。 1. 这一层是业务的核心层级。
2. 这一层按照现在的分层模式,非常独立,不会向上依赖,也不会向下依赖。
3. 这一层的对象是 Domain Object,需要与 PO (Persistence Object) 或者叫 Data Object 区分。
4. Domain Object 是带有对应的业务逻辑, PO 只是做个表的简单映射,如果是使用 ORM 工具的话,那么就对应 ORM 映射的对象。
5. 在这一层下面,可以按照业务的子领域创建各自的 package。
6. 按照 DDD 的设计**,本层使用 充血模型
7. 如何更好的设计领域对象 ( Domain Object ) 强烈推荐参考:ddd的战术篇: application service, domain service, infrastructure service阿里技术专家详解 DDD 系列 第五讲:聊聊如何避免写流水账代码
8.「不包含」UI 渲染,也「不包含」数据库或 RPC 框架的具体实现。
9. repo 的依赖,由于是 interface 注入,所以直接 mock 的方式。 ( 可以引入 Go 官方的 gomock )

本层的难点:如何定义各种各样的 Domain ObjectDomain Service
/internal/usecase Use Cases,即 DDD 中的 Application Service,它主要的作用是对 domain 业务的编排
若有必要,也可以在该 package 下面定义 子 Usecase
/internal/repo 各种数据依赖的具体实现,包括 DB、RPC、缓存等。这里面存放 PO 数据,这些数据就是 简单的表映射 这里的对象使用 失血模型 或者 贫血模型

/pkg

里面定义可以共享出去的工具。由于是可以直接让别人用,这里面的 package 当作基础依赖库使用。既然又是基础依赖库,它里面尽可能的不包含第三方依赖。


↑ top


数据规范

在整个系统中,主要分为下列几个对象:

  1. DTO (Data Transfer Object)- 数据传输对象。对应的是请求、响应的结构体对象。不存在业务逻辑,可以使用 protobuf 来约定。
  2. DO (Domain Object)- 领域对象。包含各个领域的业务逻辑。
  3. PO (Persistant Object)- 持久对象。如果是使用 ORM 的话,那么基本上对应的是表的映射对象。该对象基本上使用贫血模型,除了简单的校验逻辑外,基本上不包含任何业务逻辑。注意:在有的地方,该对象叫做:DO (Data Object)

分层数据的隔离

在实际开发过程中,有的对象从 DTO 到 DO,或者从 DO 再到 PO 对象很相似,或者一摸一样,但是对应的职责实际上是完全不同,所以必须进行完全隔离。

借用 阿里技术专家详解DDD系列 第三讲 - Repository模式 中的一张图,( 注意:下图中的 DO 实际上代表的是 PO 对象

dto_do_po_convert

  • DTO - DO - 引入 Assembler,用作 DTO 对象的组装。

  • DO - PO - 引入 Converter,用作 Domain Object 的组装。

为了避免大的结构体对象的赋值的繁琐工作,在做对象组装的时候,可以引入 jinzhu/copier 的工具辅助进行组装。( 这里一定要包含对应的单元测试!!!


↑ top


单元测试

Mock 工具 - mock

由于使用接口注入的方式,所以在做对应模块测试的时候,可以使用 golang/mock 的工具生成对应接口的 mock 实现代码。

为了便于开发过程中的生成,开发了个小工具辅助生成。安装过程为:

  1. 安装 golang/mock
go install github.com/golang/mock/mockgen@v1.6.0
  1. 下载 gmh,运行下列命令即可生成。
# 会在当前目录下,生成 xxx_mock.go 文件。
gmh -src my_interface.go

Mock工具 - mockery

mockery 配合 stretchr/testify/mock 使用,自动生成测试代码。

↑ top



依赖注入

使用 github.com/google/wire 帮助生成代码,完成依赖注入。

安装

go get github.com/google/wire/cmd/wire

使用

进入 cmd/ 目录,运行:

wire
# wire: github.com/xpzouying/go-clean-arch/cmd: wrote /Users/zy/src/zy/go-clean-arch/cmd/wire_gen.go

wire 会根据 wire.go 中的定义,自动生成 wire_gen.go 代码。

↑ top



参考资料