>From : https://studygolang.com/articles/12909
在閱讀了 Bob 叔叔的 Clean Architecture Concept 之后,我嘗試在 Golang 中實現它。我們公司也有使用相似的架構,[Kurio - App Berita Indonesia](https://kurio.co.id/), 但是結構有點不同。并不是太不同, 相同的概念,但是文件目錄結構不同。
你可以在這里找到一個示例項目[https://github.com/bxcodec/go-clean-arch](https://github.com/bxcodec/go-clean-arch),這是一個 CRUD 管理示例文章

* 免責聲明:
我不推薦使用這里的任何庫或框架,你可以使用你自己的或者第三方具有相同功能的任何框架來替換。
## 基礎
在設計簡潔架構之前我們需要了解如下約束:
1. 獨立于框架。該架構不會依賴于某些功能強大的軟件庫存在。這可以讓你使用這樣的框架作為工具,而不是讓你的系統陷入到框架的限制的約束中。
2. 可測試性。業務規則可以在沒有 UI, 數據庫,Web 服務或其他外部元素的情況下進行測試。
3. 獨立于 UI 。在無需改變系統的其他部分情況下, UI 可以輕松的改變。例如,在沒有改變業務規則的情況下,Web UI 可以替換為控制臺 UI。
4. 獨立于數據庫。你可以用 Mongo, BigTable, CouchDB 或者其他數據庫來替換 Oracle 或 SQL Server,你的業務規則不要綁定到數據庫。
5. 獨立于外部媒介。 實際上,你的業務規則可以簡單到根本不去了解外部世界。
更多詳見: [https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html](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 )
與實體( Entities )一樣, 模型會在每一層中使用,在這一層中將存儲對象的結構和它的方法。例如: Article, Student, Book。
~~~go
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 )
倉庫將存放所有的數據庫處理器,查詢,創建或插入數據庫的處理器將存放在這一層,該層僅對數據庫執行 CRUD 操作。 該層沒有業務流程。只有操作數據庫的普通函數。
這層也負責選擇應用中將要使用什么樣的數據庫。 可以是 Mysql, MongoDB, MariaDB,Postgresql,無論使用哪種數據庫,都要在這層決定。
如果使用 ORM, 這層將控制輸入,并與 ORM 服務對接。
如果調用微服務, 也將在這層進行處理。創建 HTTP 請求去請求其他服務并清理數據,這層必須完全充當倉庫。 處理所有的數據輸入,輸出,并且沒有特定的邏輯交互。
該倉庫層( Repository )將依賴于連接數據庫 或其他微服務(如果存在的話)
## 用例層( Usecase )
這層將會扮演業務流程處理器的角色。任何流程都將在這里處理。該層將決定哪個倉庫層被使用。并且負責提供數據給服務以便交付。處理數據進行計算或者在這里完成任何事。
用例層將接收來自傳遞層的所有經過處理的輸入,然后將處理的輸入存儲到數據庫中, 或者從數據庫中獲取數據等。
用例層將依賴于倉庫層。
## 表現層( Delivery )
這一層將作為表現者。決定數據如何呈現。任何傳遞類型都可以作為是 REST API, 或者是 HTML 文件,或者是 gRPC
這一層將接收來自用戶的輸入, 并清理數據然后傳遞給用例層。
對于我的示例項目, 我使用 REST API 作為表現方式。客戶端將通過網絡調用資源節點, 表現層將獲取到輸入或請求,然后將它傳遞給用例層。
該層依賴于用例層。
## 層與層之間的通信
除了模型層, 每一層都需要通過接口進行通信。例如,用例( Usecase )層需要倉庫( Repository )層,那么它們該如何通信呢?倉庫( Repository )層將提供一個接口作為他們溝通橋梁。
倉庫層( Repository )接口示例:
~~~go
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 )必須實現這個接口,以便用例層( Usecase )使用該接口。
用例層接口示例:
~~~go
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)
}
~~~
與用例層相同, 表現層將會使用這個約定接口。 并且用例層必須實現該接口。
## 測試
我們知道, 簡潔就意味著獨立。 甚至在其他層還不存在的情況下,每一層都具有可測試性。
* 模型( Models )層
該層僅測試任意結構聲明的函數或方法。 這可以獨立于其他層,輕松的進行測試。
* 倉庫( Repository )層
為了測試該層,更好的方式是進行集成測試,但你也可以為每一個測試進行模擬測試, 我使用 github.com/DATA-DOG/go-sqlmock 作為我的工具來模擬查詢過程 mysql
* 用例( Usecase )層
因為該層依賴于倉庫層, 意味著該層需要倉庫層來支持測試。所以我們根據之前定義的契約接口制作一個模擬的倉庫( Repository )模型。
* 表現( Delivery )層
與用例層相同,因為該層依賴于用例層,意味著該層需要用例層來支持測試。基于之前定義的契約接口, 也需要對用例層進行模擬。
對于模擬,我使用 vektra 的 golang的模擬庫: [https://github.com/vektra/mockery](https://github.com/vektra/mockery)
## 倉庫層(Repository)測試
為了測試這層,就如我之前所說, 我使用 sql-mock 來模擬我的查詢過程。
你可以像我一樣使用 github.com/DATA-DOG/go-sqlmock ,或者使用其他具有相似功能的庫。
~~~go
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)測試
用于用例層的示例測試,依賴于倉庫層。
~~~go
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)層尚未實現。
## 表現層( Delivery )測試
表現層測試依賴于你如何傳遞的數據。如果使用 http REST API, 我們可以使用 golang 中的內置包 httptest。
因為該層依賴于用例( Usecase )層, 所以 我們需要模擬 Usecase,與倉庫層相同,我使用 Mockery 模擬我的 Usecase 來進行表現層( Delivery )的測試。
~~~go
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 為示例:
~~~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](https://github.com/bxcodec/go-clean-arch)
我的項目中使用到的庫:
* Glide :包管理工具
* go-sqlmock from github.com/DATA-DOG/go-sqlmock
* Testify : 測試庫
* Echo Labstack (Golang Web 框架)用于 表現層
* Viper :環境配置
進一步閱讀簡潔架構 :
* [https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html](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/](http://manuel.kiessling.net/2012/09/28/applying-the-clean-architecture-to-go-applications/)。 這是Golang種另一個版本的簡潔架構。
如果你任何問題,或者需要更多的解釋,或者我在這里沒有解釋清楚的。你可以通過我的[LinkedIn](https://www.linkedin.com/in/imantumorang/)或者[email](https://studygolang.com/articles/iman.tumorang@gmail.com)聯系我。謝謝。