GO微服务 DevOps

在 Golang 上使用整洁架构(Clean Architect

2021-05-18  本文已影响0人  CatchZeng

原文:https://medium.com/hackernoon/golang-clean-archithecture-efd6d7c43047

前言

阅读完 Uncle Bob 的整洁架构(Clean Architecture)后,我尝试在 Golang 中实现它。这与我们在 Kurio-App Berita Indonesi 公司中使用的架构类似,但是结构略有不同。其实,也没什么不同,只是相同的概念但文件夹结构不同而已。

你可以在这里 https://github.com/bxcodec/go-clean-arch(CRUD 管理文章的一个示例)中查找示例项目。

image

免责声明:

我不建议在此使用任何库或框架。你可以用自己的或具有相同功能的第三方库替换此处的任何内容。

基础

整洁架构的约束条件是:

详情请参阅 https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html

注:原文中留的链接已失效,可以访问译文 https://zhuanlan.zhihu.com/p/64343082

因此,基于此约束,每一层都必须独立且可测试

Uncle Bob 的架构中包含 4 层:

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

Models

与 Entities 相同, Models 将被用在所有层.

Models 层将存储任何对象的 Struct 及其方法。示例:Article, Student, Book。

示例:

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 数据库。这里没有业务流程发生。仅是对数据库的普通功能。

Repository 层还负责选择应用程序中将使用的数据库。可能是 MysqlMongoDBMariaDBPostgresql 等。

如果使用 ORM,则此层将控制输入,并将其直接提供给 ORM 服务

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

Repository 层将取决于连接的数据库或其他微服务(如果存在)。

Usecase

Usecase 层将充当业务流程处理程序。任何过程都将在这里处理。Usecase 层将决定将使用哪个存储库层。并提供数据以供 Delivery 层使用。处理数据以进行计算等事项都将在 Usecase 层完成。

Usecase 层将接受来自 Delivery 层的任何已清理的输入,然后处理该输入,可存储到 DB 中或从 DB 中提取等。

Usecase 层将取决于 Repository 层。

Delivery

Delivery 层将充当演示者。决定如何呈现数据。可以采用 REST API 或 HTML File 或 gRPC 的形式。
Delivery 层还将接受用户的输入。清理输入并将其发送到 Usecase 层。

对于我的示例项目,我使用 REST API 作为交付方式。
客户端将通过网络调用资源终结点,Delivery 层将获取输入或请求,并将其发送到 Usecase 层。

Delivery 层将取决于 Usecase 层。

层与层之间的通信

除 Models 层外,每一层都将通过 interface 进行通信。例如,Usecase 层需要 Repository 层,那么它们如何通信?Repository 将提供一个 interface ,使其成为他们的”合同“和通讯方式。

Repository Interface 示例:

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

Usecase 层将使用此“合同”与 Repository 层通信,并且 Repository 层必须实现此接口,以便供 Usecase 层使用。

Usecase Interface 示例:

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 层将使用此”合同“接口。并且 Usecase 层必须实现此接口。

测试每一层

众所周知,整洁意味着独立每个层都具备可测性,即使其他层不存在

注:在原文中使用的 mock 工具为 https://github.com/vektra/mockery。这里更推荐大家使用 golang 官方的 https://github.com/golang/mock

测试 Repository 层

为了测试 Repository 层,就像我之前说过的那样,使用 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 层

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 层。因此,我不需要先完成我的 Repository 层。我可以先完成 Usecase 层,甚至还没有实现我的 Repository 层

测试 Delivery 层

Delivery 层测试将取决于您如何传递数据。如果使用 http REST API,则可以在 golang 中为 httptest 使用内置测试包。

因为 Delivery 层取决于 Usecase 层,所以我们需要 mock Usecase 层。与 Repository 层相同,我也使用 Mockery 模拟用例,以进行 Delivery 层测试。

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

最终输出与合并

完成所有层并已通过测试之后。你应该在 root 项目的 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"))
}

你可以看到,每一层及其相关性合并为一层。

结论

示例代码

示例项目的代码地址 https://github.com/bxcodec/go-clean-arch

用于我的项目的库:

进一步了解 Clean Architecture:

上一篇下一篇

猜你喜欢

热点阅读