這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。>(獨立性,可測試性的和簡潔性)在閱讀了 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 管理樣本文章![](https://raw.githubusercontent.com/studygolang/gctt-images/master/clean-arthitecture/1_CyteJRpIHC-DFE23UtlZfQ.png)* 免責聲明: 我不推薦使用這裡的任何庫或架構,你可以使用你自己的或者第三方具有相同功能的任何架構來替換。## 基礎在設計簡潔架構之前我們需要瞭解如下約束: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。```goimport "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 )介面樣本:```gopackage repositoryimport 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 )使用該介面。用例層介面樣本:```gopackage usecaseimport ("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 ,或者使用其他具有相似功能的庫。```gofunc 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)測試用於用例層的樣本測試,依賴於倉庫層。```gopackage usecase_testimport ("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.Articleerr := 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 為樣本:```gopackage mainimport ("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.Configfunc 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://raw.githubusercontent.com/studygolang/gctt-images/master/clean-arthitecture/1_GQdkAd7IwIwOWW-WLG5ikQ.png)* 在這裡使用的每一個庫都可以由你自己修改。因為簡潔架構的重點在於:你使用的庫不重要, 關鍵是你的架構是簡潔的,可測試的並且是獨立的。* 我項目就是這樣組織的。通過評論和分享, 你可以討論或者贊成,當然能改善它就更好了。## 樣本項目樣本項目可以在這裡看見:[ 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](iman.tumorang@gmail.com)聯絡我。謝謝。
via: https://hackernoon.com/golang-clean-archithecture-efd6d7c43047
作者:Iman Tumorang 譯者:fredvence 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
783 次點擊