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

使用传统的分层架构,我们的所有依赖项都指向一个方向,上面的每一层都依赖于下面的层。传输层将依赖于交互器,交互器将依赖于持久层。

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

概述

最近在想着写一个个人项目,但是在项目的结构上却犯了难,此时翻到了一个视频,采用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:

package portstype 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:

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:

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)
}

之后适配器实现:

type Adapter struct {// depedencies injectionarith ports.ArithmeticPortdb    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:

// internal/ports/framework_right.go
package portstype DBPort interface {CloseDBConnection()AddToHistory(answer int32, operation string) error
}

然后实现:

//internal/adapters/framework/right/db/db.go
package dbtype Adapter struct {db *sql.DB
}func NewAdapter(driverName, dataSourceName string) (*Adapter, error) {// connect to dbdb, 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 ]

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/256100.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

http与apache

目录 1.http相关概念 2.http请求的完整过程 3.访问浏览器背后的原理过程 4.动态页面与静态页面区别 静态页面: 动态页面: 5.http协议版本 6.http请求方法 7.HTTP协议报文格式 8.http响应状态码 1xx:提示信息 2xx:成功…

2-4、DEBUG和源程序区别

语雀原文链接 文章目录 1、DEBUG 和 汇编编译器MASM区别1:默认进制不同区别2:[地址]示例1:debug示例2:[0]示例3:[寄存器]示例4:ds:[0]小结 区别3:源程序数据不能以字母开头 1、DEBUG 和 汇编编…

【二叉树】

文章目录 树形结构注意要点细分概念树在生活中的应用 二叉树什么是二叉树二叉树特点:两种特殊的二叉树二叉树的性质二叉树性质的练习二叉树的存储二叉树的遍历前序遍历中序遍历后序遍历遍历练习 树形结构 树是一种非线性的数据结构,它具有以下的特点&am…

class059 建图、链式前向星、拓扑排序【算法】

class059 建图、链式前向星、拓扑排序【算法】 code1 建图 package class059;import java.util.ArrayList; import java.util.Arrays;public class Code01_CreateGraph {// 点的最大数量public static int MAXN 11;// 边的最大数量// 只有链式前向星方式建图需要这个数量// 注…

Unity 关于Ray、RaycastHit、Raycast及其使用

Unity中,我们要进行物理模拟和碰撞检测时,有三个重要的概念Ray、RaycastHit、Raycast。 其中,Ray可以理解为射线,它是一条从起点沿着特定方向延伸的无限长线段。 它的语法是: Ray(Vector3 origin, Vector3 directio…

多路径传输(MPTCP MPQUIC)数据包调度研究总结

近些年来,以5G和Wifi6为代表的无线通信技术发展迅速,并已经在全世界实现了大规模部署。此外,智能手机等移动设备不断迭代更新,其网络通信能力也持续演进,使得应用同时利用多个不同网卡在多条不同物理链路上&#xff08…

2023-2024-1-高级语言程序设计-第2次月考编程题

注:此前已发布过的题解不再发布(原题请在下面位置进行搜索)。 7-1-2 排序(算法任意) 本题要求将给定的n个整数从大到小排序后输出(可使用任意排序算法)。 输入格式: 输入第一行给出一个不超过10的正整数n。第二行给…

【Docker】Swarm的ingress网络

Docker Swarm Ingress网络是Docker集群中的一种网络模式,它允许在Swarm集群中运行的服务通过一个公共的入口点进行访问。Ingress网络将外部流量路由到Swarm集群中的适当服务,并提供负载均衡和服务发现功能。 在Docker Swarm中,Ingress网络使…

如何使用HadSky搭配内网穿透工具搭建个人论坛并发布至公网随时随地可访问

文章目录 前言1. 网站搭建1.1 网页下载和安装1.2 网页测试1.3 cpolar的安装和注册 2. 本地网页发布2.1 Cpolar临时数据隧道2.2 Cpolar稳定隧道(云端设置)2.3 Cpolar稳定隧道(本地设置)2.4 公网访问测试 总结 前言 经过多年的基础…

ORA-600 kcbzib_kcrsds_1一键恢复

一个19c库由于某种原因redo损坏强制打开库报ORA-600 kcbzib_kcrsds_1错误 SQL> startup mount pfile?/database/pfile.txt; ORACLE instance started. Total System Global Area 859830696 bytes Fixed Size 9034152 bytes Variable Size 5…

Android平板还能编程?Ubuntu本地安装code-server远程编程写代码

文章目录 1.ubuntu本地安装code-server2. 安装cpolar内网穿透3. 创建隧道映射本地端口4. 安卓平板测试访问5.固定域名公网地址6.结语 1.ubuntu本地安装code-server 准备一台虚拟机,Ubuntu或者centos都可以,这里以VMwhere ubuntu系统为例 下载code server服务,浏览器…