Go整洁架构理论
Go整洁架构理论
做者:donghli,腾讯PCG后台开发工程师
| 导语领会过 Hex 六边形架构、Onion 洋葱架构、Clean 整洁架构的同窗能够将本篇文章介绍的理论办法与本身项目代码架构比照并互通有无,配合改进。没领会过上述架构的同窗能够进修一种新的架构办法,并测验考试将其利用到营业项目中,降低项目庇护成本,进步效率。
本文提及的架构次要指项目组织的“代码架构”,重视与微办事架构等名词中的办事架构停止区分。
1.为什么要有代码架构
汗青悠久的项目大城市有良多开发人员参与“奉献”,在没有好的批示规则约束的情状下,大致会酿成一团乱麻。 剪不竭,理还乱,也没有勇士开发者情愿往剪往理。被迫接手的勇士开发者假设想要增加一个小需求,可能需要花10倍的时间往理顺营业逻辑,再花10倍的时间往填补测试代码,其实是低效又痛苦。
那是一个普及的痛点问题,也有无数开发者测验考试过往处理它。那么多年开展累积下来,业界天然也降生了良多软件架构。各人耳熟能详的就有六边形架构(Hexagonal Architecture),洋葱架构(Onion Architecture),整洁架构(Clean Architecture)等。那些架构在细节上必定有所差别,但是核心目标都是一致的:努力于实现软件系统的存眷点别离(separation of concerns)。
存眷点别离之后的软件系统都具备如下特征:
* 不依靠特定UI。UI能够肆意替代,不会影响系统重其他组件。从 Web UI 酿成桌面 UI,以至酿成掌握台 UI 都无所谓,营业逻辑不会被影响。
* 不依靠特定框架。以Java生态举例,不论是利用web框架 koa, express,仍是利用桌面利用框架 electron,仍是掌握台框架 commander,营业逻辑都不会被影响,被影响的只会是框架接进的那一层。
* 不依靠特定外部组件。系统能够肆意利用 MySQL, MongoDB, 或 Neo4j 做为数据库,肆意利用 Redis, Memcached, 或 etcd 做为键值存储等。营业逻辑不会因为那些外部组件的替代而改变。
* 随便测试。核心营业逻辑能够在不需要 UI,不需要数据库,不需要 Web 办事器等一切外界组件的情状下被测试。那种地道的代码逻辑意味着清晰随便的测试。
软件系统有了那些特征后,易于测试,更易于庇护、更新,大大减轻了软件开发人员的心智承担。所以,好的代码架构确实值得推崇。
2.好的代码架构是若何构建的
前文所述的三个架构在理念上是近似的,从下文图1到图3三幅架构图中也能看出类似的圈层构造。图中能够看到,越往外层越详细,越往内层越笼统。那也意味着,越往外越有可能发作改变,包罗但不限于框架晋级,中间件变动,适配新末端等等。
展开全文
图 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 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 部门稍有差别。
同前两者齐名的六边形架构,固然外形不是齐心圆,但是构造上仍是有良多唤应的处所。
图 3 Hexagon Architecture, Andrew Gordon
图3六边形架构中灰色箭头表达依靠注进(Dependency Injection),其与整洁架构中的依靠规则(The Dependency Rule)有异曲同工之妙,也限造了整个架构各组件的依靠标的目的必需是“由外向内”。图中的各类 Port 和 Adapter 是六边形架构的重中之重,故该架构别称 Ports and Adapters。
图 3 Hexagon Architecture, Andrew Gordon
如图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 的 发送到外部办事。
其实,不只国外有优良的代码架构,国内也有。
国内开发者在进修了六边形架构,洋葱架构和整洁架构之后,提出了 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 语言项目脚手架理论计划。
3.选举一种 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 //(可选)广义上第三方办事的拜候实现,能够通过 // 各层可共享的公共组件代码
由此目次构造能够看出通过 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.goimport "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.goimport "mybusiness.com/blog-api/domain/model" // 依靠同层的 modeltype BlogManager interface { //定义核心营业逻辑的接口办法 Load(...) ... Save(...) ... ...}
Domain 层是核心层,不会依靠任何外层组件,只能层内依靠。那也保障了 Domain 层的地道,保障了整个软件系统的可庇护性。
// Infrastructure 层 database/mysql.goimport ( "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 设想的系统分层架构能够一层一层地将营业恳求剥离清洁,别离处置后再一层一层地组拆好返回到恳求方。各层之间互不骚乱,职责清楚,有效地降低了系统组件之间的耦合,提拔了系统的可庇护性。
4.总结
无论哪种架构都不会是项目开发的银弹,也不会有百试百灵的开发办法论。事实引进一种架构是有必然复杂度和较高庇护成本的,所以开发者需要根据本身项目类型揣度能否需要引进架构。
不料见引进架构的项目类型:
* 软件生命周期可能率会小于三个月的
* 项目庇护人员在如今以及可见的未来只要本身的
能够考虑引进架构的项目类型:
* 软件生命周期可能率会大于三个月的
* 项目庇护人员多于1人的
强烈定见引进架构的项目类型:
* 软件生命周期可能率会大于三年的
* 项目庇护人员多于5人的
5. 参考文献
[1] Robert C. Martin, The Clean Architecture, )
[2] Andrew Gordon, Clean Architecture, )
[3] Pablo Martinez, Hexagonal Architecture, there are always two sides to every story, )
[5] Jeffrey Palermo, The Onion Architecture, )