尝试Golang的简洁架构(解耦、可测试、简洁)

原文:https://hackernoon.com/golang-clean-archithecture-efd6d7c43047(须翻墙)

在阅读了Bob的Clean Architecture Concept之后,我试图在Golang中实现它。这是一个类似的架构,在我们的公司Kurio-App Berita Indonesia中使用过,但结构有点不同。没有太多差异,相同的概念,但文件夹结构不同。

你可以在这里找到一个示例项目https://github.com/bxcodec/go-clean-arch,这是一个CRUD管理文章例子。

image

免责声明:

我不推荐这里使用的任何库或框架。你可以在这里替换任何东西,使用你自己或第三方库里的具有相同的功能的模块。

基本原则

正如我们所知道的,在设计Clean Architecture之前的约束是:

1、独立于框架。该体系结构不依赖于某些功能强大的软件库的存在。这使您可以使用这样的框架作为工具,而不必将系统塞进有限的约束中。

2、可测试。业务规则可以在没有UI,数据库,Web服务器或任何其他外部元素的情况下进行测试。

3、独立于用户界面。用户界面可以轻松更改,而无需更改系统的其余部分。例如,Web UI可以替换为控制台UI,而无需更改业务规则。

4、独立于数据库。您可以使用Mongo,BigTable,CouchDB或其他更换你现在的Oracle或SQL Server。您的业​​务规则不绑定到数据库。

5、独立于任何外部代理。事实上,你的业务规则根本就不了解外面的世界。

更多:https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html

所以,基于这个约束规则,每一层都必须是独立的,可测试的。

在Bob的架构里有4层:

Entities(实体)

Usecase(用例)

Controller(控制器)

Framework & Driver(框架和驱动)

在我的项目中,我也使用了4个:

Models (实体)

Repository(持久化)

Usecase(用例)

Delivery(分发)

Models

与实体相同,models将用于所有层。该层将存储任何对象的Struct和它的方法。例如:文章,学生,书。

示例结构:

import "time"

type Article struct {
ID        int64    `json:"id"`
Title    string    `json:"title"`
Content  string    `json:"content"`
UpdatedAt time.Time `json:"updated_at"`
CreatedAt time.Time `json:"created_at"`
}

任何实体或模型都将存储在此处。

Repository

Repository将存储任何数据库处理程序。查询或创建/插入任何数据库将存储在此处。此层仅对CRUD执行数据库操作。这里没有业务流程发生,只有针对数据库的一些基本简单操作。
该层也有责任选择在应用程序中使用的数据库。可能是Mysql,MongoDB,MariaDB,Postgresql无论如何,都会在这里决定。
如果使用ORM,该层将控制输入,并将其直接提供给ORM服务。

如果调用微服务,将在这里处理。创建HTTP请求到其他服务,并清理数据。该层必须完全充当存储库。处理所有数据输入 - 输出没有特定的逻辑发生。

此存储库层将依赖于连接DB或其他微服务(如果存在)。

Usecase
该层将充当业务流程处理程序。任何过程都将在这里处理。该层将决定使用哪个存储库层。并有责任提供数据以便交付。处理数据进行计算或在这里完成任何事情。

用例层将接受来自传递层的任何已经过处理的输入,然后处理输入可以存储到数据库中,也可以从数据库中获取输入等。

这个Usecase层将依赖于Repository Layer

Delivery
这一层将负责显示,决定数据如何呈现。可以是REST API、HTML或gRPC。 该层也将接受来自用户的输入,校验输入并将其发送到Usecase。

在我的示例项目,我使用REST API作为分发方法。 客户端将通过网络调用资源端点,传递层将获取输入或请求,并将其发送到用例层。

该层将依赖于Usecase层。

层之间的通信
除了模型,每个图层都将通过inteface进行通信。例如,Usecase层需要存储库层,因此它们如何通信?存储库将提供一个接口作为他们的联系和沟通。

存储层接口的示例

package repository

import models "github.com/bxcodec/go-clean-arch/article"

type ArticleRepository interface {

Fetch(cursor string, num int64) ([]*models.Article, error)

GetByID(id int64) (*models.Article, error)

GetByTitle(title string) (*models.Article, error)

Update(article *models.Article) (*models.Article, error)

Store(a *models.Article) (int64, error)

Delete(id int64) (bool, error)

}

用例层将使用该合同与Repository进行通信,并且Repository层必须实现此接口,以便Usecase可以使用该接口

用例界面示例


package usecase

import (

"github.com/bxcodec/go-clean-arch/article"

)

type ArticleUsecase interface {

Fetch(cursor string, num int64) ([]*article.Article, string, error)

GetByID(id int64) (*article.Article, error)

Update(ar *article.Article) (*article.Article, error)

GetByTitle(title string) (*article.Article, error)

Store(*article.Article) (*article.Article, error)

Delete(id int64) (bool, error)

}

与Usecase相同,Delivery Layer将使用此接口。而Usecase层必须实现这个接口。

Testing Each Layer

我们知道,简洁意味着独立。每层都是可测试的甚至其他层还不存在。

模型图层

此图层仅在任何Struct中声明的任何函数/方法进行测试。

并且可以轻松测试并独立于其他层。

存储库

为了测试这一层,更好的方法是进行集成测试。但是你也可以为每个测试做mocking 。我使用github.com/DATA-DOG/go-sqlmock作为我的助手来模拟查询过程msyql。

用例

因为这个层依赖于Repository层,意味着这个层需要Repository层进行测试。所以我们必须

根据之前定义的契约接口制作一个嘲笑嘲笑的Repository模型。

传递

与Usecase相同,因为此图层依赖于Usecase图层,这意味着我们需要使用Usecase图层进行测试。而且基于之前定义的契约接口,用例层也必须mocking

对于mocking ,我使用vektra对golang的mocking 可以在这里看到https://github.com/vektra/mockery

存储层测试

为了测试这个层,就像我之前说过的,我使用了一个sql-mock来模拟我的查询过程。你可以像我在这里使用的那样使用github.com/DATA-DOG/go-sqlmock或者其他具有类似功能的东西


func TestGetByID(t *testing.T) {

db, mock, err := sqlmock.New()

if err != nil {

t.Fatalf(“an error ‘%s’ was not expected when opening a stub

database connection”, err)

}

defer db.Close()

rows := sqlmock.NewRows([]string{

“id”, “title”, “content”, “updated_at”, “created_at”}).

AddRow(1, “title 1”, “Content 1”, time.Now(), time.Now())

query := “SELECT id,title,content,updated_at, created_at FROM

article WHERE ID = \\?”

mock.ExpectQuery(query).WillReturnRows(rows)

a := articleRepo.NewMysqlArticleRepository(db)

num := int64(1)

anArticle, err := a.GetByID(num)

assert.NoError(t, err)

assert.NotNil(t, anArticle)

}

用例测试

用于Usecase层的样本测试,依赖于Repository层。


package usecase_test

import (

"errors"

"strconv"

"testing"

"github.com/bxcodec/faker"

models "github.com/bxcodec/go-clean-arch/article"

"github.com/bxcodec/go-clean-arch/article/repository/mocks"

ucase "github.com/bxcodec/go-clean-arch/article/usecase"

"github.com/stretchr/testify/assert"

"github.com/stretchr/testify/mock"

)

func TestFetch(t *testing.T) {

mockArticleRepo := new(mocks.ArticleRepository)

var mockArticle models.Article

err := faker.FakeData(&mockArticle)

assert.NoError(t, err)

mockListArtilce := make([]*models.Article, 0)

mockListArtilce = append(mockListArtilce, &mockArticle)

mockArticleRepo.On("Fetch", mock.AnythingOfType("string"), mock.AnythingOfType("int64")).Return(mockListArtilce, nil)

u := ucase.NewArticleUsecase(mockArticleRepo)

num := int64(1)

cursor := "12"

list, nextCursor, err := u.Fetch(cursor, num)

cursorExpected := strconv.Itoa(int(mockArticle.ID))

assert.Equal(t, cursorExpected, nextCursor)

assert.NotEmpty(t, nextCursor)

assert.NoError(t, err)

assert.Len(t, list, len(mockListArtilce))

mockArticleRepo.AssertCalled(t, "Fetch", mock.AnythingOfType("string"), mock.AnythingOfType("int64"))

}

Mockery会为我生成一个存储库层的模型。所以我不需要先完成我的Repository层。我可以先完成我的Usecase,即使我的Repository层尚未实现。

交付测试

交付测试将取决于您如何交付数据。如果使用http REST API,我们可以在golang中为httptest使用httptest内置包。

因为它取决于Usecase,所以我们需要模拟Usecase。和Repository一样,我也使用Mockery来模拟我的用例,进行传递测试。


func TestGetByID(t *testing.T) {

var mockArticle models.Article

err := faker.FakeData(&mockArticle)

assert.NoError(t, err)

mockUCase := new(mocks.ArticleUsecase)

num := int(mockArticle.ID)

mockUCase.On(“GetByID”, int64(num)).Return(&mockArticle, nil)

e := echo.New()

req, err := http.NewRequest(echo.GET, “/article/” +

strconv.Itoa(int(num)), strings.NewReader(“”))

assert.NoError(t, err)

rec := httptest.NewRecorder()

c := e.NewContext(req, rec)

c.SetPath(“article/:id”)

c.SetParamNames(“id”)

c.SetParamValues(strconv.Itoa(num))

handler:= articleHttp.ArticleHandler{

AUsecase: mockUCase,

Helper: httpHelper.HttpHelper{}

}

handler.GetByID(c)

assert.Equal(t, http.StatusOK, rec.Code)

mockUCase.AssertCalled(t, “GetByID”, int64(num))

}

最终产出和合并

完成所有图层并已通过测试。您应该在根项目中将main.go合并为一个系统。

在这里,您将定义并创建每个环境需求,并将所有图层合并为一个。

以我的main.go为例:


package main

import (

"database/sql"

"fmt"

"net/url"

httpDeliver "github.com/bxcodec/go-clean-arch/article/delivery/http"

articleRepo "github.com/bxcodec/go-clean-arch/article/repository/mysql"

articleUcase "github.com/bxcodec/go-clean-arch/article/usecase"

cfg "github.com/bxcodec/go-clean-arch/config/env"

"github.com/bxcodec/go-clean-arch/config/middleware"

_ "github.com/go-sql-driver/mysql"

"github.com/labstack/echo"

)

var config cfg.Config

func init() {

config = cfg.NewViperConfig()

if config.GetBool(`debug`) {

fmt.Println("Service RUN on DEBUG mode")

}

}

func main() {

dbHost := config.GetString(`database.host`)

dbPort := config.GetString(`database.port`)

dbUser := config.GetString(`database.user`)

dbPass := config.GetString(`database.pass`)

dbName := config.GetString(`database.name`)

connection := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", dbUser, dbPass, dbHost, dbPort, dbName)

val := url.Values{}

val.Add("parseTime", "1")

val.Add("loc", "Asia/Jakarta")

dsn := fmt.Sprintf("%s?%s", connection, val.Encode())

dbConn, err := sql.Open(`mysql`, dsn)

if err != nil && config.GetBool("debug") {

fmt.Println(err)

}

defer dbConn.Close()

e := echo.New()

middL := middleware.InitMiddleware()

e.Use(middL.CORS)

ar := articleRepo.NewMysqlArticleRepository(dbConn)

au := articleUcase.NewArticleUsecase(ar)

httpDeliver.NewArticleHttpHandler(e, au)

e.Start(config.GetString("server.address"))

}

你可以看到,每个图层都与它的依赖关系合并成一个图层。

结论:

总之,如果画在一张图上,可以看到下面

image

在这里使用的每个库都可以由您自己更改。因为简洁的架构的主要观点是:不关注你的库,但你的架构是简洁的,可测试也是独立的。

这就是我组织我的项目的方式,你可以反对或者同意,或者可以改善这个更好,请留下评论并分享给大家。

示例项目

示例项目可以在这里看到https://github.com/bxcodec/go-clean-arch

项目中用到的库:

Glide : for package management

go-sqlmock from github.com/DATA-DOG/go-sqlmock

Testify : for testing

Echo Labstack (Golang Web Framework) for Delivery layer

Viper : for environment configurations

进一步阅读关于简洁架构:
https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html

http://manuel.kiessling.net/2012/09/28/applying-the-clean-architecture-to-go-applications/.

Another version of Clean Architecture in Golang

如果您有任何疑问,或需要更多解释,或者我无法在这里解释清楚,您可以通过我的LinkedIn通过电子邮件发送给我。谢谢

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,265评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,274评论 1 288
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,087评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,479评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,782评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,218评论 1 207
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,594评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,316评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,955评论 1 237
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,274评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,803评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,177评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,732评论 3 229
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,953评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,687评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,263评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,189评论 2 258

推荐阅读更多精彩内容