说起简洁架构
,不得不先说另外两个也很著名的架构 洋葱架构
和 六边形架构
,它们都有共同的思想
以下是他们的简介,有兴趣可以通过最下方的链接查看原文
又名 Ports & Adapters
由阿利斯泰尔·科伯恩 (Alistair Cockburn)
提出,如名称显示,就是端口与适配器实现架构为的是把业务代码与基础设施隔离,核心逻辑与外部依赖隔离.
如下显示,application
不与外部接触,所以交流都通过port
和adapter
来实现,即使外部设施发生改变也不会影响到application
, 同时由于不依赖外部设施,在进行单元测试的时候,即使没有DB
和HTTP
等其他外部设施也能进行
此时的架构还只是一个理论思想,并没有像后来的架构提创的各种分层以及各种名词的定义
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7ZZRdQOr-1662273335661)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f29b3ba175084494886c665b76c5c2ab~tplv-k3u1fbpfcp-watermark.image?)]
由杰弗里·巴勒莫 (Jeffrey Palermo)
提出,有趣的是他在文章中反复强调这个架构不是他的百分百原创,只是在总结平时大家的使用的各种架构方式,并在这个基础上提出这个架构,一个很重要的目的是为了能在沟通的时候使用一个大家都能理解的名词交流
文章中他指出了洋葱架构和传统架构的差异,传统的三层架构中,我们不会区分业务代码和外部设施同时每个层都会耦合外部设施,万一外部设施发生改变,每个层涉及的代码都会发生改变,此处指的外部设施包括(日历,db,远程调用等)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-znTAaiTF-1662273335662)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5cf0038002bf4118859d344971fd4b30~tplv-k3u1fbpfcp-watermark.image?)]
提出了洋葱架构的同时还贴心的提出了翻译版本的图
application
可以在缺少外部设施下运行[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JEX3o1A6-1662273335663)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ef025913dccb4f6f9ca836c67297bd05~tplv-k3u1fbpfcp-watermark.image?)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4ugQdNY2-1662273335663)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/69078e79b6fb4083a0b8d6879002b234~tplv-k3u1fbpfcp-watermark.image?)]
由 罗伯特·C·马丁 (Robert Cecil Martin)
提出,此时已经可以把我们的项目划分为基本的四层架构,分别是核心Entities
,Use Cases
,为了跟外部设施进行交流所做的适配器层Controllers,Gateways等
,最后就是外部设施Web,DB等
一个最基本的工作流,应该是 web
-> Controllers
-> Use Cases
-> Entities
-> Gateways
-> DB
,然后依次返回
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lA3cIBwf-1662273335664)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e0b2d16827be4bb585fc35ad972cc5d8~tplv-k3u1fbpfcp-watermark.image?)]
集合部分业务规则,既可以是有方法的对象,也可以是一组数据结构或者方法的组合
包含应用程序的业务规则,封装并实现了所用的用例, 负责编排实体的数据更新或者使用他们的特定业务逻辑去实现目的
这一层的变动不应该影响entity,但它也不应该受ui,database,框架的影响而改变
主要是 port 和 adapter 适配器功能,用于将数据转化为更适合外圈数据需要的层
同时这一层也将外部数据 database, http 转化为 entity 的作用
到了这里基本把三个架构都简单的介绍了一遍,后面就是按照上面介绍的思想,用于实践到我们的项目中来
| -- main.go # 项目启动入口, 调用service各业务的new方法
| -- entity
| -- log # 定义常用的通用方法,例如: 日志,隔离核心代码与外部设施的依赖
| -- gift # 每个业务定义一个文件夹,存在各个实体
| -- logic
| -- gift # 每个业务定义一个文件夹
| -- port.go # 定义接口,供service调用
| -- adapter.go # port的具体实现
| -- repository
| -- gift
| -- adapter.go # 实现gateway的port
| -- assembler.go # entity与外部设施(mysql,redis,http)沟通的数据结构转换器,非必须
| -- mysql # mysql/redis/http 视各自业务而定
| -- port.go # 接口文件
| -- service # 服务入口,建议各自业务定义一个文件
| -- gift.go # 调用logic的interface, 发起提问的作用
这里我写了一个例子 送礼物
里面会包括基本的分层,日志,db,外部接口调用等例子
假设我们有一个项目叫做送礼物,其中有个两个模块 gift
和 gift_config
, gift
负责给某个人送礼物,gift_config
负责配置给哪些人送礼物
AddGiftConfig
增加礼物配置
func (a *Adapter) AddGiftConfig(ctx context.Context, cmd AddGiftConfigCMD) error {
entity := &giftEntity.ConfigEntity{}
if err := a.giftConfigPort.Create(ctx, entity); err != nil {
log.ErrorContextf(ctx, "xxx error:", err)
return err
}
// 更新缓存
_ = async.Go(ctx, 3*time.Second, func(cloneCtx context.Context) {
_ = a.giftConfigPort.RefreshCache(cloneCtx)
})
return nil
}
AddGift
判断某个人满足配置条件,发送礼物
func (a *Adapter) AddGift(ctx context.Context, uin uint64) error {
configs := a.giftConfigPort.List(ctx, giftconfig.GiftConfigQRY{})
for _, c := range configs {
if c.AllowSendGift() {
insertData := &giftEntity.Entity{}
if err := a.giftPort.Create(ctx, insertData); err != nil {
return err
}
return nil
}
}
return errors.New("not allow gift")
}
虽然分层的名称,概念各不相同,但是大家都有意将业务代码与外部依赖隔离开,并且依赖方向是向内的,只要把握这两条原则,其他就可以自己根据业务需要各自实现了
Reference: