项目架构-六边形架构的概述和实现

使用传统的分层架构,我们的所有依赖项都指向一个方向,上面的每一层都依赖于下面的层。传输层将依赖于交互器,交互器将依赖于持久层。
在六边形架构中,所有依赖项都指向内部------我们的核心业务逻辑对传输层或数据源一无所知。尽管如此,传输层知道如何使用交互器,数据源知道如何符合存储库接口。

概述

最近在想着写一个个人项目,但是在项目的结构上却犯了难,此时翻到了一个视频,采用Hexagonal architecture(六边形架构),也被称为Ports and Adapters,大致就是下面图片的结构:

一共分为三层:

Domain: 里面放的是处理的基本逻辑,可以理解为大纲,它决定着Application和Framework的选择和实现

Application::它协调使用我们的Domain代码, 通过位于两者之间的方式,调整从framework到domain的请求

Framework: 为外部组件提供交互方式,驱动通常放在左边,被驱动放在右边

我们需要注意的是:

  1. 高层模块不应该依赖于低层模块。两者都应该依赖于抽象。
  2. 抽象不应该依赖于细节。细节应该依赖于抽象。
  3. 在驱动侧,适配器依赖于端口,由应用程序服务实现,因此适配器不知道谁会响应其调用,它只知道有哪些方法是保证可用的,因此它依赖于抽象。
  4. 在被驱动侧,应用程序服务依赖于端口,而适配器则实现端口的接口,有效地反转了依赖关系,因为"低级"适配器(即数据库存储库)被迫实现应用程序核心中定义的抽象,这是"高级"的。

所以我们的项目目录会像这样:

例子

这里我们也能看出六边形架构的另外一个称呼:Ports and Adapters的原因,适配器实现端口(通常为接口),以达到代码解耦的作用,下面将以上面的目录进行具体的例子讲解:

完整代码:link

本项目很简单,就是实现一个简单加减乘除的运算和数据库保存,那么我们秉持着核心domain层统领一切,适配器实现端口的原则,我们先定义 ./ports/core.go:

go 复制代码
package ports

type ArithmeticPort interface {
	Addition(a int32, b int32) (int32, error)
	Subtraction(a int32, b int32) (int32, error)
	Multiplication(a int32, b int32) (int32, error)
	Division(a int32, b int32) (int32, error)
}

有了接口我们就得配以适配器 ./adapters/core/arithmetic/arithmetic.go:

go 复制代码
type Adapter struct {
}

func NewAdapter() *Adapter {
	return &Adapter{}
}

func (Arith Adapter) Addition(a int32, b int32) (int32, error) {
	return a + b, nil
}

func (Arith Adapter) Subtraction(a int32, b int32) (int32, error) {
	return a - b, nil
}

func (Arith Adapter) Multiplication(a int32, b int32) (int32, error) {
	return a * b, nil
}

func (Arith Adapter) Division(a int32, b int32) (int32, error) {
	return a / b, nil
}

这便是我们的核心逻辑,当项目慢慢变大时,核心层逻辑也会越来越多。

接下来就到了应用层,当我们实现了运算,那么便需要拿到结果,注意:此时还用不到sql,所以我们把目的写进 ./ports/app.go:

go 复制代码
type APIPort interface {
	GetAddition(a int32, b int32) (int32, error)
	GetSubtraction(a int32, b int32) (int32, error)
	GetMultiplication(a int32, b int32) (int32, error)
	GetDivision(a int32, b int32) (int32, error)
}

之后适配器实现:

go 复制代码
type Adapter struct {
	// depedencies injection
	arith ports.ArithmeticPort
	db    ports.DBPort
}

func NewAdapter(db ports.DBPort, arith ports.ArithmeticPort) *Adapter {
	return &Adapter{
		db:    db,
		arith: arith,
	}
}

func (api Adapter) GetAddition(a int32, b int32) (int32, error) {
	answer, err := api.arith.Addition(a, b)
	err = api.db.AddToHistory(answer, "addition")
	if err != nil {
		return 0, err
	}
	return answer, nil
}

func (api Adapter) GetSubtraction(a int32, b int32) (int32, error) {
	answer, err := api.arith.Subtraction(a, b)
	err = api.db.AddToHistory(answer, "subtraction")
	if err != nil {
		return 0, err
	}
	return answer, nil
}

func (api Adapter) GetMultiplication(a int32, b int32) (int32, error) {
	return api.arith.Multiplication(a, b)
}

func (api Adapter) GetDivision(a int32, b int32) (int32, error) {
	return api.arith.Division(a, b)
}

然后就到了用依赖的时候了,也就是framework,本文就讲讲mysql的CRUD:

go 复制代码
// internal/ports/framework_right.go
package ports

type DBPort interface {
	CloseDBConnection()
	AddToHistory(answer int32, operation string) error
}

然后实现:

go 复制代码
//internal/adapters/framework/right/db/db.go
package db

type Adapter struct {
	db *sql.DB
}

func NewAdapter(driverName, dataSourceName string) (*Adapter, error) {
	// connect to db
	db, err := sql.Open(driverName, dataSourceName)
	...
}

func (da Adapter) AddToHistory(answer int32, operation string) error {
	stmt, err := da.db.Prepare("INSERT INTO history (data, answer, opration) VALUES (?,?,?)")
	...
}

之后我们编写测试文件,进行测试,通常情况一个适配器配一个测试文件

基本都创建好之后,如何连接呢?

我们在cmd文件中创建一个main.go:连接所有端口和适配器代码的地方,将依赖项注入到需要的层中,例如将数据库注入到framework层

这样实现了代码的解耦,例如我们想换一个数据库,只需要更换数据库名和数据源名,其余不需要修改,同时我们的业务逻辑也不需要了解特定的数据源限制

总结

所以总结一下优点:

  1. 能够封装数据源实现细节
  2. 长期稳定性和可扩展性,因为只需少许更改,所以在微服务部署失败时很容易回滚,也可以直接通过配置文件决定数据源

但是他也并不是silver bullet,我们应该多多检测层之间的漏洞,预防逻辑泄露等问题

参考文章&&学习视频

Ready for changes with Hexagonal Architecture

Hexagonal Architecture, there are always two sides to every story

How To Structure Your Go App - Full Course Hex Arch + Tests

相关推荐
木木学AI5 分钟前
2026主流大模型电话机器人系统解析:技术架构、业务执行与企业落地实践
架构·系统架构·机器人
ChampaignWolf25 分钟前
在 SAP S/4HANA 内部基于 ICF Handler 从零实现 ABAP MCP Server:架构、代码与踩坑实录
c++·架构·sap·abap·mcp
国科安芯36 分钟前
星链供电架构中的高集成度电源管理方案研究——基于ASP4644四通道降压稳压器在低轨卫星星座中的应用分析
大数据·单片机·嵌入式硬件·架构·低轨卫星·星间链路
Bs_MoneyMagnet41 分钟前
基于springboot+vue的医疗健康便民服务平台的设计与实现 源码+文档
java·vue.js·spring boot·后端·spring
诚心呈意共享外卖配送系统44 分钟前
中秋礼品配送峰值场景下,自建同城配送平台的技术选型与架构实践
架构·系统架构·创业创新
爱吃火鸡面呀1 小时前
HDFS 架构深度解析:从核心组件到数据读写全流程
hadoop·hdfs·架构
wuyk5551 小时前
从零吃透 MQTT 通信|第 8 章 FreeRTOS 多任务架构下 MQTT 工程架构,任务拆分、队列解耦、临界区保护
c语言·开发语言·stm32·学习·架构
彧azz2 小时前
Redis 全面指南:从核心概念到高可用架构实战
数据库·redis·架构
SLD_Allen2 小时前
MoE架构原理与算力基础设施重估
架构·moe
子兮曰2 小时前
Bun v1.4.1 深度解析:从 Zig 到 Rust,一场 11 天、64 个 AI 代理的语言迁徙
前端·后端·bun