美文网首页golang我爱编程
尝试Golang的简洁架构(解耦、可测试、简洁)

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

作者: devabel | 来源:发表于2018-04-17 11:15 被阅读452次

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

    相关文章

      网友评论

      本文标题:尝试Golang的简洁架构(解耦、可测试、简洁)

      本文链接:https://www.haomeiwen.com/subject/hjeukftx.html