原文:https://hackernoon.com/golang-clean-archithecture-efd6d7c43047(须翻墙)
在阅读了Bob的Clean Architecture Concept之后,我试图在Golang中实现它。这是一个类似的架构,在我们的公司Kurio-App Berita Indonesia中使用过,但结构有点不同。没有太多差异,相同的概念,但文件夹结构不同。
你可以在这里找到一个示例项目https://github.com/bxcodec/go-clean-arch,这是一个CRUD管理文章例子。
免责声明:
我不推荐这里使用的任何库或框架。你可以在这里替换任何东西,使用你自己或第三方库里的具有相同的功能的模块。
基本原则
正如我们所知道的,在设计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"))
}
你可以看到,每个图层都与它的依赖关系合并成一个图层。
结论:
总之,如果画在一张图上,可以看到下面
在这里使用的每个库都可以由您自己更改。因为简洁的架构的主要观点是:不关注你的库,但你的架构是简洁的,可测试也是独立的。
这就是我组织我的项目的方式,你可以反对或者同意,或者可以改善这个更好,请留下评论并分享给大家。
示例项目
示例项目可以在这里看到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