腾小云导读
为了下降体系组件之间的耦合、进步体系的可保护性,一个好的代码结构显得尤为重要。本文将为咱们介绍众所周知的三种代码结构,并从三种结构引申出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.4COLA架构
国内开发者在学习了六边形架构、洋葱架构和整齐架构之后,提出了 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,blog.cleancoder.com/uncle-bob/2…(2012)
[2] Andrew Gordon, Clean Architecture,www.andrewgordon.me/posts/Clean…(2021)
[3] Pablo Martinez, Hexagonal Architecture, there are always two sides to every story,medium.com/ssense-tech…(2021)
[4] 张建飞, COLA 4.0:运用架构的最佳实践,blog.csdn.net/significant…(2022)
[5] Jeffrey Palermo, The Onion Architecture,jeffreypalermo.com/2008/07/the…(2008)
以上是本次分享悉数内容,欢迎咱们在谈论区别享交流。假如觉得内容有用,欢迎转发~
-End-
原创作者|donghui
技能责编|donghui
“怎么更好的下降体系组件之间的耦合、进步体系的可保护性”是让开发者们亘古不变的头疼问题,除了设计好的代码架构,容器化技能等也是重要的解耦技能。咱们还能想到哪些能够下降体系耦合度,进步体系可保护性的办法呢?
欢迎在谈论区聊一聊你的观点。在4月4日前将你的谈论记载截图,发送给腾讯云开发者大众号后台,可收取腾讯云「开发者春季限制红包封面」一个,数量有限先到先得 。咱们还将选取点赞量最高的1位朋友,送出腾讯QQ公仔1个。4月4日中午12点开奖。快约请你的开发者朋友们一起来参加吧!
最近微信改版啦
许多开发者朋友反馈收不到咱们更新的文章
咱们能够关注并点亮星标
不再错过小云的知识速递
阅读原文