独立,可测试和清洁
阅读了叔叔Bob的“干净架构概念”后,我正在尝试在Golang中实现它。 这与我们在Kurio-App Berita Indonesi公司中使用的架构类似,但结构略有不同。 没什么不同,相同的概念但文件夹结构不同。
您可以在这里https://github.com/bxcodec/go-clean-arch(CRUD管理示例文章)中查找示例项目。
免责声明:
我不建议在此使用任何库或框架。 您可以用自己的或具有相同功能的第三方替换此处的任何内容。
基本的
我们知道在设计Clean Architecture之前的约束是:
- 独立于框架。 该体系结构不依赖于某些功能丰富的软件库的存在。 这使您可以将此类框架用作工具,而不必将系统塞入有限的约束中。
- 可测试的。 可以在不使用UI,数据库,Web服务器或任何其他外部元素的情况下测试业务规则。
- 独立于UI。 UI可以轻松更改,而无需更改系统的其余部分。 例如,可以在不更改业务规则的情况下用控制台UI替换Web UI。
- 独立于数据库。 您可以将Oracle或SQL Server换成Mongo,BigTable,CouchDB或其他东西。 您的业务规则未绑定到数据库。
- 独立于任何外部机构。 实际上,您的业务规则根本对外界一无所知。更多信息请参见https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html
因此,基于此约束,每一层都必须独立且可测试。
如果Bob叔叔的体系结构具有4层:
- 实体
- 用例
- 控制者
- 框架和驱动
在我的项目中,我也使用了4:
- 楷模
- 资料库
- 用例
- 交货
楷模
与实体相同,将在所有图层中使用。 该层将存储任何对象的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"`
}
任何实体或模型都将存储在此处。
资料库
存储库将存储任何数据库处理程序。 查询或创建/插入任何数据库将存储在此处。 该层仅适用于CRUD数据库。 这里没有业务流程发生。 仅普通功能对数据库。
该层还负责选择应用程序中将使用的数据库。 可能是Mysql,MongoDB,MariaDB,Postgresql等,将在这里决定。
如果使用ORM,则此层将控制输入,并将其直接提供给ORM服务。
如果调用微服务,将在这里处理。 创建对其他服务的HTTP请求,并清理数据。 该层必须完全充当存储库。 处理所有数据输入-输出没有发生特定的逻辑。
此存储库层将取决于Connected DB或其他微服务(如果存在)。
用例
该层将充当业务流程处理程序。 任何过程都将在这里处理。 该层将决定将使用哪个存储库层。 并有责任提供数据以供交付使用。 处理数据以进行计算,否则将在此处完成任何操作。
用例层将接受来自交付层的任何已消毒的输入,然后处理该输入可存储到DB或从DB中提取等。
该用例层将取决于存储库层
交货
该层将充当演示者。 决定如何呈现数据。 可以采用REST API或HTML File或gRPC的形式(无论采用哪种交付方式)。
该层还将接受用户的输入。 清理输入,并将其发送到用例层。
对于我的示例项目,我使用REST API作为交付方式。
客户端将通过网络调用资源端点,传递层将获取输入或请求,并将其发送到用例层。
该层将取决于用例层。
层间通讯
除模型外,每一层都将通过接口进行通信。 例如,用例层需要存储库层,那么它们如何通信? 储存库将提供一个界面,使其成为他们的合同和通讯方式。
存储库接口示例
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)
}
用例层将使用此合同与存储库通信,并且存储库层必须实现此接口,以便供用例使用
用例接口示例
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)
}
与用例相同,交付层将使用此合同接口。 用例层必须实现此接口。
测试每一层
众所周知,清洁意味着独立。 每个可测试的层甚至其他层都不存在。
- 模型层:仅在Struct的任何函数中声明了任何函数/方法时,此层才进行测试。 并且可以轻松地进行测试并且独立于其他层。
- 存储库:为了测试这一层,更好的方法是进行集成测试。 但是您也可以为每个测试进行模拟。 我正在使用github.com/DATA-DOG/go-sqlmock作为我的助手来模拟查询过程msyql。
- 用例:由于该层取决于存储库层,因此意味着该层需要存储库层进行测试。 因此,我们必须基于之前定义的协定接口,制作一个以嘲笑为原型的存储库模型。
- 交付:与用例相同,因为该层取决于用例层,这意味着我们需要用例层进行测试。 而且,Usecase层还必须基于之前定义的协定接口,以嘲笑来嘲弄
对于嘲讽,我在vektra上对golang使用嘲讽,可以在这里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)
}
用例测试
用例层的样本测试,取决于存储库层。
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将为我生成一个存储库层模型。 因此,我不需要先完成我的存储库层。 即使尚未实现我的存储库层,我也可以先完成用例。
交付测试
传递测试将取决于您如何传递数据。 如果使用http REST API,则可以在golang中为httptest使用内置测试包。
因为这取决于Usecase,所以我们需要模拟Usecase。 与存储库相同,我也使用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))
}
最终输出与合并
完成所有层并已通过测试之后。 您应该在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
用于我的项目的库:
- Glide:用于包裹管理
- 来自github.com/DATA-DOG/go-sqlmock的go-sqlmock
- 证明:用于测试
- 交付层的Echo Labstack(Golang Web框架)
- 毒蛇:用于环境配置
进一步了解Clean Architecture:
- 本文的第二部分:https://hackernoon.com/trying-clean-architecture-on-golang-2-44d615bf8fdf
- 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/。 Golang的Clean Architecture的另一个版本
如果您有疑问,或者需要更多的解释,或者说,我不能很好地解释在这里,你可以问我从我的 LinkedIn 或 电邮 我。 谢谢
From: https://hackernoon.com/golang-clean-archithecture-efd6d7c43047