CleanArchitecture.jpg

简介

洋葱模型是近期流行的、业务垂直划分的架构模型。

由于Go语言特有的包依赖特性,在实现洋葱模型时,代码布局和目录结构和其他语言(如C/C++/Java/Python)有所不同。

本文给出了一个用Go语言实现洋葱模型架构的例子,并阐述了这个例子背后的设计原理。

注:本文不涉及Go项目整体布局方案。

太长不看版

一个典型的业务需求是「添加订单」。组网结构如下:

graph LR client[/http客户端/] orders[orders] client --http请求--> orders --写入--> mysql mysql[(mysql服务器)]

要实现的是中间这个orders模块的「添加订单」功能,包括5个步骤:

  1. 接受「添加订单」的HTTP请求。
  2. 解析出请求中的订单信息。
  3. 判断订单是否合法。
  4. 将订单持久化到MySQL。
  5. 返回HTTP响应。

这个模块Go的洋葱模型实现如下(不包含web框架的启动部分)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
orders
├── http/echo
│   └── echo.go
├── orders.go
├── model.go
├── repo.go
└── repo/mysql
    ├── mysql.go
    └── po.go

这个实现包含了3个包。

package 路径 描述
orders orders包定义了订单结构体Order、核心结构体Service、保存订单的接口RepoService实现了函数AddOrder(o *Order),这个函数判断订单的合法性,然后调用Repo的方法SaveOrder
orders/http/echo echo包负责从HTTP请求中解析出订单对象orders.Order,然后调用方法orders.AddOrder。这个包实现了http服务器框架echo的handler。
orders/repo/mysql mysql包实现了接口orders.Repo,将订单对象orders.Order实际写入MySQL数据库。这个包依赖Go-MySQL-Driver以及某个ORM框架(如xorm)。

用六边形架构来理解的话,echo就是「输入适配器」,负责在echo框架和orders之间适配;而mysql就是「输出适配器」,负责在orders和mysql-driver以及xorm之间适配。

包之间的实际依赖注入,以及将orders/http/echo/实现的handler函数注入echo框架,都由外部的代码(如main函数)实现。

具体依赖关系如下图所示:

graph LR xorm{{github.com/go-xorm}} mysqlDriver{{github.com/go-sql-driver/mysql}} echo{{github.com/labstack/echo}} subgraph orders/http/echo HttpHandler end HttpHandler -.-> echo HttpHandler -.-> Service HttpHandler -.-> Order subgraph orders/repo/mysql OrderRepo end OrderRepo -.-> Order OrderRepo -.-> mysqlDriver OrderRepo -.-> xorm subgraph orders Service Order Repo[/Repo/] Service -.-> Order Repo -.-> Order Service -.-> Repo end

注意:orders/repo/mysql里的OrderRepo并不直接依赖orders.Repo接口,这是因为Go语言特有的鸭子模型思想,struct只要实现了interface就可以完成接口的实现,而不需要在语言里显式地标明struct和interface之间的关系。

核心思想

要理解这个洋葱模型的实现,需要理解Go语言里包的特性。

  1. Go一个目录里只能有一个package。
  2. Go没有「子package」的概念,package和package之间只有直接或间接import的关系。
  3. 一个目录的package,和该目录下子目录的package,彼此之间完全无关,没有任何依赖或约束。(这一点和Java、Python都不一样)。

由此,就要改变其他语言里「同一层的代码放在同一个目录」的惯性思维,忘记「层」的概念,转化成「以package为唯一代码组织单元」的思想。

package应该自身保证自己在功能上的高内聚;而package所在的目录路径,只不过是用于描述package的功能名称,以减少代码里大量的业务名词。

在设计package的时候要记住:所谓package,就是接收「不明来源、不知何时」的 外部调用,经过内部的逻辑处理,在「业务逻辑规定的时间」用「业务逻辑规定的参数」调用「不知被谁、用什么方式实现了的」包内接口

(不用读)一个更复杂的orders

下面给出一个稍微复杂一点的orders的布局,除了http输入和mysql输出,还增加了mq输入、redis输出、mq输出,需要查询用户表,还需要通过MQ上报统计结果。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
orders/
├── http/echo
│   ├── add.go # 「添加订单」的http handler。
│   ├── echo.go # 比较通用的一些定义。
│   └── query.go # 「查询订单」的http handler。
├── mq/rabbitmq/
│   ├── mq.go
│   └── stat.go  # 从MQ收到的「订单统计」请求消息的处理函数。
├── model.go # Order struct DO
├── orders.go # Service struct。因为似乎每个包下面都要有一个同名的Go文件。
├── errors.go # 统一的哨兵error定义。
├── stat.go # 上报订单统计结果的interface。
├── repo.go # 用户信息、订单信息的数据库读写interface。
├── repo/customer/
│   │   ├── mysql
│   │   │   ├── mysql.go # 用户信息的MySQL读写实现。
│   │   │   └── po.go # 用户信息的MySQL PO。
│   │   └── redis
│   │       ├── po.go # 用户信息的Redis PO。
│   │       └── redis.go # 用户信息的redis读写实现。
│   └── order
│       ├── mysql
│       │   ├── mysql.go # 订单信息的MySQL读写实现。
│       │   └── po.go # 订单信息的MySQL PO。
│       └── redis
│           ├── po.go # 订单信息的Redis PO。
│           └── redis.go # 订单信息的Redis读写实现。
└── stat/mq
    └── mq.go # 实现了「上报订单统计」接口,这里是通过MQ上报。

这个结构可以和DDD的事件总线无缝对接,在输入适配器里添加EventHandler,在输出适配器里添加写EventBusWriter。

总结

这篇文章描述了洋葱模型在Go语言的一种实现方式,并描述了实现背后的思想。在实际工作中,需要根据这些原理,针对具体需求定制布局。

引用

[1] Clean Architecture in Go https://medium.com/@hatajoe/clean-architecture-in-go-4030f11ec1b1 [2] 阿里巴巴Java开发手册 https://github.com/alibaba/p3c [3] Go Microservice with Clean Architecture: Application Design https://medium.com/@jfeng45/ [4] Hexagonal (Ports & Adapters) Architecture https://medium.com/idealo-tech-blog/hexagonal-ports-adapters-architecture-e3617bcf00a0 [5] Duck typing in Go https://medium.com/@matryer/golang-advent-calendar-day-one-duck-typing-a513aaed544d