icon-cookie
The website uses cookies to optimize your user experience. Using this website grants us the permission to collect certain information essential to the provision of our services to you, but you may change the cookie settings within your browser any time you wish. Learn more
I agree
blank_error__heading
blank_error__body
Text direction?

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

0.627字数 2,157阅读 2,733

原文: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通过电子邮件发送给我。谢谢

Measure
Measure
Related Notes
Get a free MyMarkup account to save this article and view it later on any device.
Create account

End User License Agreement

Summary | 5 Annotations
REST API、HTML或gRPC
2020/07/13 09:45
每个图层都将通过inteface进行通信
2020/07/13 09:46
简洁意味着独立
2020/07/13 09:46
https://github.com/vektra/mockery
2020/07/13 09:48
sqlmock
2020/07/13 09:48