关于架构设计:Golang整洁架构实践

3次阅读

共计 9771 个字符,预计需要花费 25 分钟才能阅读完成。

腾小云导读

为了升高零碎组件之间的耦合、晋升零碎的可维护性,一个好的代码框架显得尤为重要。本文将为大家介绍家喻户晓的三种代码框架,并从三种框架引申出 COLA 架构以及作者基于 COLA 架构设计的 Go 语言我的项目脚手架实际计划。心愿能给宽广开发爱好者带来帮忙和启发!


看目录,点珍藏

1. 为什么要有代码架构

2. 好的代码架构是如何构建的

2.1 整洁架构

2.2 洋葱架构

2.3 六边形架构

2.4 COLA 架构

3. 举荐一种 Go 代码架构实际

4. 总结

* 本文提及的架构次要指我的项目组织的“代码架构”,留神与微服务架构等名词中的服务架构进行辨别。

01、为什么要有代码架构

历史悠久的我的项目大都会有很多开发人员参加“奉献”,在没有好的领导规定束缚的状况下,大抵会变成一团乱麻。剪一直,理还乱,也没有开发壮士违心去剪去理。被迫接手的开发壮士如果想要减少一个小需要,可能须要花 10 倍的工夫去理顺业务逻辑,再花 10 倍的工夫去补充测试代码,切实是低效又苦楚。

这是一个广泛的痛点问题,有有数开发者尝试过来解决它。这么多年倒退下来,业界天然也诞生了很多软件架构。大家耳熟能详的就有六边形架构(Hexagonal Architecture),洋葱架构(Onion Architecture),整洁架构(Clean Architecture)等。

这些架构在细节上有所差别,然而外围指标是统一的:致力于实现软件系统的 关注点拆散(separation of concerns)。

关注点拆散之后的软件系统都具备如下特色:

  • 不依赖特定 UI。 UI 能够任意替换,不会影响零碎中其余组件。从 Web UI 变成桌面 UI,甚至变成控制台 UI 都无所谓,业务逻辑不会被影响。
  • 不依赖特定框架。 以 JavaScript 生态举例,不论是应用 web 框架 koa、express,还是应用桌面利用框架 electron,还是控制台框架 commander,业务逻辑都不会被影响,被影响的只会是框架接入的那一层。
  • 不依赖特定内部组件。 零碎能够任意应用 MySQL、MongoDB 或 Neo4j 作为数据库,任意应用 Redis、Memcached 或 etcd 作为键值存储等。业务逻辑不会因为这些内部组件的替换而变动。
  • 容易测试。 外围业务逻辑能够在不须要 UI、不须要数据库、不须要 Web 服务器等所有外界组件的状况下被测试。这种纯正的代码逻辑意味着清晰容易的测试。

软件系统有了这些特色后,易于测试,更易于保护、更新,大大加重了软件开发人员的心理累赘。所以,好的代码架构值得推崇。

02、好的代码架构是如何构建的

前文所述的三个架构在理念上是近似的,从下文图 1 到图 3 三幅架构图中也能看出类似的圈层构造。图中能够看到,越往外层越具体,越往内层越形象。这也意味着,越往外越有可能发生变化,包含但不限于框架降级、中间件变更、适配新终端等等。

2.1 整洁架构

图 1 The Clean Architecture, Robert C. Martin

图 1 整洁架构 的同心圆构造中能够看见三条由内向内的彩色箭头,它示意依赖规定(The Dependency Rule)。依赖规定规定外层的代码能够依赖内层,然而内层的代码不能够依赖外层。也就是说内层逻辑不能够依赖任何外层定义的变量、函数、构造体、类、模块等等代码实体。如果最外层蓝色层“Frameworks & Drivers”DB 处应用了 go 语言的 gorm 三方库,并定义了 gorm 相干的数据库构造体及其 tag 等。那么内层的 Gateways、Use Cases、Entities 等处不能够援用任何外层中 gorm 相干的构造体或办法,甚至不应该感知到 gorm 的存在。

核心层的 Entities 定义示意外围业务规定的外围业务实体。这些实体既能够是带办法的类,也能够是带有一堆函数的构造体。但它们必须是高度形象的,只能够随着外围业务规定而变动,不能够随着外层组件的变动而变动。以简略博客零碎举例的话,此层能够定义 Blog、Comment 等外围业务实体。

type Blog struct {...}
type Comment struct {...}
  • 核心层的外层是利用业务层

利用业务层的 Use Cases 应该蕴含软件系统的所有业务逻辑。该层管制所有流向和流出核心层的数据流,并应用核心层的实体及其业务规定来实现业务需要。此层的变更不会影响核心层、更外层的变更,例如开发框架、数据库、UI 等变动,也不会影响此层。接着博客零碎的例子,此层能够定义 BlogManager 接口,并定义其中的 CreateBlog, LeaveComment 等业务逻辑办法。

type BlogManager interface {CreateBlog(...) ...
    LeaveComment(...) ...}
  • 利用业务层的外层是接口适配层

接口适配层的 Controllers 将外层输出的数据格式转换成内层 Use Cases 和 Entities 方便使用的格局,而后 Presenters,Gateways 再将内层处理结果转换成外层方便使用的格局,而后再由更外层出现到 Web、UI 或者写入到数据库。如果零碎抉择关系型数据库做为其长久化计划的话,那么所有对于 SQL 的解决都应该在此层实现,更内层不须要感知到任何数据库的存在。

同理,如果零碎与外界服务通信,那么所有无关外界服务数据的转化都在此层实现,更内层也不须要感知到外界服务的存在。外层通过此层传递数据个别通过 DTO(Data Transfer Object)或者 DO(Data Object)实现。接上文博客零碎例子,示例代码如下:

type BlogDTO struct { // Data Transfer Object
    Content string `json:"..."`
}

// DTO 与 model.Blog 的转化在此层实现

func CreateBlog(b *model.Blog) {dbClient.Create(&blog{...})
 ...}
  • 接口适配层的外层是处在最外层的框架和驱动层

该层蕴含具体的框架和依赖工具的细节,例如零碎应用的数据库、Web 框架、音讯队列等等。此层次要帮忙内部的框架、工具,和内层进行数据连接。接博客零碎例子,框架和驱动层如果应用 gorm 来操作数据库,则相干的示例代码如下:

import "gorm.io/driver/mysql"
import "gorm.io/gorm"

type blog struct { // Data Object
    Content string `gorm:"..."` // 本层的数据库 ORM 如果替换,此处的 tag 也须要随之扭转
} 
type MySQLClient struct {DB *gorm.DB}
func New(...) {gorm.Open(...) ... }
func Create(...)...

至此,整洁架构图中的四层已介绍实现。但此图中的四层构造仅作示意,整洁架构并不要求软件系统必须依照此四层结构设计。只有软件系统能保障“由内向内”的依赖规定,零碎的层数多少可自在决定。

整体构造与洋葱架构二者齐名且结构图类似,都是四层同心圆。

2.2 洋葱架构

图 2 Onion Architecture, Jeffrey Palermo

图 2 中 洋葱架构 最外围的 Domain Model 为组织中外围业务的状态及其行为模型,与整洁架构中的 Entities 高度一致。

其外层的 Domain Services 与整洁架构中的 Use Cases 职责相近。更外层的 Application Services 桥接 UI 和 Infrastructue 中的数据库、文件、内部服务等,与整洁架构中的 Interface Adaptors 性能雷同。最边缘层的 User Interface 与整洁架构中的最外层 UI 局部统一,Infrastructure 则与整洁架构中的 DB,Devices,External Interfaces 作用统一,只有 Tests 局部稍有差别。

同前两者齐名的六边形架构,尽管形状不是同心圆,然而构造上还是有很多对应的中央。

2.3 六边形架构

图 3 Hexagon Architecture, Andrew Gordon

图 3 六边形架构 中灰色箭头示意依赖注入(Dependency Injection),其与整洁架构中的依赖规定(The Dependency Rule)有殊途同归之妙,也限度了整个架构各组件的依赖方向必须是“由内向内”。图中的各种 Port 和 Adapter 是六边形架构的重中之重,故该架构别称 Ports and Adapters。

图 4 Hexagon Architecture Phase 1, Pablo Martinez

如图 4 所示,在六边形架构中,来自驱动边(Driving Side)的用户或内部零碎输出指令通过右边的 Port & Adapter 达到利用零碎,解决后,再通过左边的 Adapter & Port 输入到被驱动边(Driven Side)的数据库和文件等。

Port 是零碎的一个与具体实现无关的入口,该入口定义了外界与零碎通信的接口(interface)。Port 不关怀接口的具体实现,就像 USB 端口容许多种设施通过其与电脑通信,但它不关怀设施与电脑之间的照片、视频等等具体数据是如何编解码传输的。

图 5 Hexagon Architecture Phase 2, Pablo Martinez

如图 5 所示,Adapter 负责 Port 定义的接口的技术实现,并通过 Port 发动与利用零碎的交互。例如,图左 Driving Side 的 Adapter 能够是一个 REST 控制器,客户端通过它与利用零碎通信。图右 Driven Side 的 Adapter 能够是一个数据库驱动,利用零碎的数据通过它写入数据库。此图中能够看到,尽管六边形架构看上去与整洁架构不那么类似,但其利用零碎核心层的 Domain、边缘层的 User Interface 和 Infrastructure 与整洁架构中的 Entities 和 Frameworks & Drivers 齐全是一一对应的。

再次回到图 3 的六边形架构整体图:

以 Java 生态为例,Driving Side 的 HTTP Server In Port 能够承接来自 Jetty 或 Servlet 等 Adapter 的申请,其中 Jetty 的申请能够是来自其余服务的调用。既处在 Driving Side 又处在 Driven Sides 中的 Messaging In/Out Port 能够承接来自 RabbitMQ 的事件申请,也能够将 Application Adapters 中生成的数据写入到 RabbitMQ。Driven Side 的 Store Out Port 能够将 Application Adapters 产生的数据写入到 MongoDB;HTTP Client Out Port 则能够将 Application Adapters 产生的数据通过 JettyHTTP 发送到内部服务。

其实,不仅国外有优良的代码架构,国内也有。

2.4 COLA 架构

国内开发者在学习了六边形架构、洋葱架构和整洁架构之后,提出了 COLA(Clean Object-oriented and Layered Architecture)架构,其名称含意为「整洁的基于面向对象和分层的架构」。它的核心理念与国外三种架构雷同,都是提倡以业务为外围,解耦内部依赖,拆散业务复杂度和技术复杂度[4]。整体架构模式如图 6 所示。

图 6 COLA 架构, 张建飞

尽管 COLA 架构不再是同心圆或者六边形的模式,然而还是能显著看到前文三种架构的影子。Domain 层中 model 对应整洁架构的 Entities、六边形架构和洋葱架构中的 Domain Model。Domain 层中 gateway 和 ability 对应整洁架构的 Use Cases、六边形架构中的 Application Logic 以及洋葱架构中的 Domain Services。App 层则对应整洁架构 Interface Adapters 层中的 Controllers、Gateways 和 Presenters。最上方的 Adapter 层和最下方的 Infrastructure 层合起来与整洁架构的边缘层 Frameworks & Drivers 绝对应。

Adapter 层上方的 Driving adater 与 Infrastructure 层下方的 Driven adapter 更是与六边形架构中的 Driving Side 和 Driven Side 高度类似。

COLA 架构在 Java 生态中落地已久,也为开发者们提供了 Java 语言的 archetype,可不便地用于 Java 我的项目脚手架代码的生成。笔者受其启发,推出了一种合乎 COLA 架构规定的 Go 语言我的项目脚手架实际计划。

03、举荐一种 Go 代码架构实际

我的项目目录构造如下:

├── adapter // Adapter 层,适配各种框架及协定的接入,比方:Gin,tRPC,Echo,Fiber 等
├── application // App 层,解决 Adapter 层适配过后与框架、协定等无关的业务逻辑
│ ├── consumer //(可选)解决内部音讯,比方来自音讯队列的事件生产
│ ├── dto // App 层的数据传输对象,外层达到 App 层的数据,从 App 层登程到外层的数据都通过 DTO 流传
│ ├── executor // 解决申请,包含 command 和 query
│ └── scheduler //(可选)解决定时工作,比方 Cron 格局的定时 Job
├── domain // Domain 层,最外围最纯正的业务实体及其规定的形象定义
│ ├── gateway // 畛域网关,model 的外围逻辑以 Interface 模式在此定义,交由 Infra 层去实现
│ └── model // 畛域模型实体
├── infrastructure // Infra 层,各种内部依赖,组件的连接,以及 domain/gateway 的具体实现
│ ├── cache //(可选)内层所需缓存的实现,能够是 Redis,Memcached 等
│ ├── client //(可选)各种中间件 client 的初始化
│ ├── config // 配置实现
│ ├── database //(可选)内层所需长久化的实现,能够是 MySQL,MongoDB,Neo4j 等
│ ├── distlock //(可选)内层所需分布式锁的实现,能够基于 Redis,ZooKeeper,etcd 等
│ ├── log // 日志实现,在此接入第三方日志库,防止对内层的净化
│ ├── mq //(可选)内层所需音讯队列的实现,能够是 Kafka,RabbitMQ,Pulsar 等
│ ├── node //(可选)服务节点一致性协调控制实现,能够基于 ZooKeeper,etcd 等
│ └── rpc //(可选)狭义上第三方服务的拜访实现,能够通过 HTTP,gRPC,tRPC 等
└── pkg // 各层可共享的公共组件代

由此目录构造能够看出通过 Adapter 层屏蔽外界框架、协定的差别,Infrastructure 层囊括各种中间件和内部依赖的具体实现,App 层负责组织输出、输入,Domain 层能够齐全聚焦在最纯正也最不容易变动的外围业务规定上。

依照前文 infrastructure 中目录构造,各子目录中文件样例参考如下:

├── infrastructure
│ ├── cache
│ │ └── redis.go // Redis 实现的缓存
│ ├── client
│ │ ├── kafka.go // 构建 Kafka client
│ │ ├── mysql.go // 构建 MySQL client
│ │ ├── redis.go // 构建 Redis client(cache 和 distlock 中都会用到 Redis,对立在此构建)
│ │ └── zookeeper.go // 构建 ZooKeeper client
│ ├── config
│ │ └── config.go // 配置定义及其解析
│ ├── database
│ │ ├── dataobject.go // 数据库操作依赖的数据对象
│ │ └── mysql.go // MySQL 实现的数据长久化
│ ├── distlock
│ │ ├── distributed_lock.go // 分布式锁接口,在此是因为 domain/gateway 中没有间接须要此接口
│ │ └── redis.go // Redis 实现的分布式锁
│ ├── log
│ │ └── log.go // 日志封装
│ ├── mq
│ │ ├── dataobject.go // 音讯队列操作依赖的数据对象
│ │ └── kafka.go // Kafka 实现的音讯队列
│ ├── node
│ │ └── zookeeper_client.go // ZooKeeper 实现的一致性协调节点客户端
│ └── rpc
│ ├── dataapi.go // 第三方服务拜访性能封装
│ └── dataobject.go // 第三方服务拜访操作依赖的数据对象

再接前文提到的博客零碎例子,假如用 Gin 框架搭建博客零碎 API 服务的话,架构各层相干目录内容大抵如下:

// Adapter 层 router.go,路由入口
import (
    "mybusiness.com/blog-api/application/executor" // 向内依赖 App 层

    "github.com/gin-gonic/gin"
)

func NewRouter(...) (*gin.Engine, error) {r := gin.Default()
  r.GET("/blog/:blog_id", getBlog)
  ...
}

func getBlog(...) ... {
  // b's type: *executor.BlogOperator
  result := b.GetBlog(blogID)
  // c's type: *gin.Context
  c.JSON(..., result)}

如代码所体现,Gin 框架的内容会被全副限度在 Adapter 层,其余层不会感知到该框架的存在。

// App 层 executor/blog_operator.go
import "mybusiness.com/blog-api/domain/gateway" // 向内依赖 Domain 层

type BlogOperator struct {blogManager gateway.BlogManager // 字段 type 是接口类型,通过 Infra 层具体实现进行依赖注入}

func (b *BlogOperator) GetBlog(...) ... {blog, err := b.blogManager.Load(ctx, blogID)
    ...
    return dto.BlogFromModel(...) // 通过 DTO 传递数据到外层}

App 层会依赖 Domain 层定义的畛域网关,而畛域网关接口会由 Infra 层的具体实现注入。外层调用 App 层办法,通过 DTO 传递数据,App 层组织好输出交给 Domain 层解决,再将失去的后果通过 DTO 传递到外层。

// Domain 层 gateway/blog_manager.go
import "mybusiness.com/blog-api/domain/model" // 依赖同层的 model

type BlogManager interface { // 定义外围业务逻辑的接口办法
  Load(...) ...
  Save(...) ...
  ...
}
 Domain 层是核心层,不会依赖任何外层组件,只能层内依赖。这也保障了 Domain 层的纯正,保障了整个软件系统的可维护性。// Infrastructure 层 database/mysql.go
import (
    "mybusiness.com/blog-api/domain/model" // 依赖内层的 model
    "mybusiness.com/blog-api/infrastructure/client" // 依赖同层的 client
)

type MySQLPersistence struct {client client.SQLClient // client 中已构建好了所需客户端,此处不必引入 MySQL, gorm 相干依赖}

func (p ...) Load(...) ... { // Domain 层 gateway 中接口办法的实现
  record := p.client.FindOne(...)
  return record.ToModel() // 将 DO(数据对象)转成 Domain 层 model}

Infrastructure 层中接口办法的实现都须要将后果的数据对象转化成 Domain 层 model 返回,因为畛域网关 gateway 中定义的接口办法的入参、出参只能蕴含同层的 model,不能够有外层的数据类型。

前文提及的残缺调用流程如图 7 所示。

图 7 Blog 读取过程时序示意图

如图,内部申请首先到达 Adapter 层。如果是读申请,则携带简略参数来调用 App 层;如果是写申请,则携带 DTO 调用 App 层。App 层将收到的 DTO 转化成对应的 Model 调用 Domain 层 gateway 相干业务逻辑接口办法。因为零碎初始化阶段曾经实现依赖注入,接口对应的来自 Infra 层的具体实现会解决实现并返回 Model 到 Domain 层,再由 Domain 层返回到 App 层,最终经由 Adapter 层将响应内容出现给内部。

至此可知,参照 COLA 设计的零碎分层架构能够一层一层地将业务申请剥离洁净,别离解决后再一层一层地组装好返回到申请方。各层之间互不烦扰,职责明显,无效地升高了零碎组件之间的耦合,晋升了零碎的可维护性。

04、总结

无论哪种架构都不会是我的项目开发的银弹,也不会有百试百灵的开发方法论。毕竟引入一种架构是有肯定复杂度和较高保护老本的,所以开发者须要依据本身我的项目类型判断是否须要引入架构:

不倡议引入架构的我的项目类型:

  • 软件生命周期大概率会小于三个月的
  • 我的项目保护人员在当初以及可见的未来只有本人的

能够思考引入架构的我的项目类型:

  • 软件生命周期大概率会大于三个月的
  • 我的项目保护人员多于 1 人的

强烈建议引入架构的我的项目类型:

  • 软件生命周期大概率会大于三年的
  • 我的项目保护人员多于 5 人的

参考文献:

[1] Robert C. Martin, The Clean Architecture, https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html (2012)

[2] Andrew Gordon, Clean Architecture, https://www.andrewgordon.me/posts/Clean-Architecture/ (2021)

[3] Pablo Martinez, Hexagonal Architecture, there are always two sides to every story, https://medium.com/ssense-tech/hexagonal-architecture-there-are-always-two-sides-to-every-story-bc0780ed7d9c (2021)

[4] 张建飞, COLA 4.0:利用架构的最佳实际, https://blog.csdn.net/significantfrank/article/details/110934799 (2022)

[5] Jeffrey Palermo, The Onion Architecture, https://jeffreypalermo.com/2008/07/the-onion-architecture-part-1/ (2008)

以上是本次分享全部内容,欢送大家在评论区分享交换。如果感觉内容有用,欢送转发~

-End-

原创作者|donghui

技术责编|donghui

“如何更好的升高零碎组件之间的耦合、晋升零碎的可维护性”是让开发者们亘古不变的头疼问题,除了设计好的代码架构,容器化技术等也是重要的解耦技术。大家还能想到哪些能够升高零碎耦合度,进步零碎可维护性的办法呢?

欢送在评论区聊一聊你的认识。在 4 月 4 日前将你的评论记录截图,发送给腾讯云开发者公众号后盾,可支付腾讯云「开发者秋季限定红包封面」一个,数量无限先到先得。咱们还将选取点赞量最高的 1 位敌人,送出腾讯 QQ 公仔 1 个。4 月 4 日中午 12 点开奖。快邀请你的开发者敌人们一起来参加吧!

最近微信改版啦

很多开发者敌人反馈收不到咱们更新的文章

大家能够 关注并点亮星标

不再错过小云的常识速递

正文完
 0