热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

用Go实现简洁架构(译文)|Go主题月

用Go实现简洁架构(译文)|Go主题月-在阅读了uncleBob的简洁架构概念之后,我尝试用Golang实现它。这是我们公司Kurio-AppBeritaIndonesia使用的类

在阅读了uncle Bob 的简洁架构概念之后,我尝试用 Golang 实现它。这是我们公司 Kurio-App Berita Indonesia 使用的类似架构,没有太大的不同,相同的概念但文件夹结构略有不同。

您可以在这里查找示例项目 github.com/bxcodec/go-… 一篇关于 CRUD 管理的文章。

  • 免责声明:

    我不推荐这里使用任何库或框架。你可以用你自己的或者第三方的具有相同功能的东西来替换这里的任何东西。

基本

正如我们所知,在设计简洁的架构之前,约束条件是:

  1. 独立于框架。该体系结构并不依赖于某个功能丰富的软件库的存在。这允许您将这些框架用作工具,而不必将系统塞进它们有限的约束中。

  2. 可测试。业务规则可以在没有 UI、数据库、Web 服务器或任何其他外部元素的情况下进行测试。

  3. 独立于用户界面。用户界面可以很容易地更改,而无需更改系统的其余部分。例如,可以用控制台 UI 替换 Web UI,而无需更改业务规则。

  4. 独立于数据库。您可以将 Oracle 或 SQL Server 换成 Mongo、BigTable、CouchDB 或其他东西。您的业务规则未绑定到数据库。

  5. 独立于任何外部机构。事实上,你的业务规则根本就不了解外部世界。

更多:8thlight.com/blog/uncle-…

因此,基于这个约束,每一层都必须是独立的和可测试的。

如果 Uncle Bob 的架构,那么会有以下4层:

  • Entities
  • Usecase
  • Controller
  • Framework & Driver

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

  • Models
  • Repository
  • Usecase
  • Delivery

Models

与实体相同,将在所有层中使用。此层将存储任何对象的结构及其方法。例句: 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 数据库起作用。这里没有业务流程。只对数据库执行普通函数。

该层还负责选择应用程序中使用的数据库。可能是 Mysql,MongoDB,MariaDB,Postgresql 等等,都会在这里决定。 如果使用 ORM,该层将控制输入,并将其直接提供给 ORM 服务。

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

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

Usecase

这个层将充当业务流程处理程序。任何过程都会在这里处理。这个层将决定使用哪个存储库层。并有责任提供数据的交付。处理数据,进行计算,或者在这里完成任何操作。

Usecase 层将接受来自交付层的任何输入,这些输入已经被处理,然后处理输入可以存储到 DB 中,或者从 DB 中提取,等等。

Usecase 层依赖于Repository 层。

Delivery

此层将充当演示者。决定如何呈现数据。可以是 REST APIHTML 文件或 gRPC,无论交付类型如何。 该层还将接受用户的输入。清理输入并将其发送到Usecase 层。

对于我的示例项目,我使用 REST API 作为交付方法。

客户端将通过网络调用资源端点,Delivery 层将获取输入或请求,并将其发送到Usecase 层。

Delivery 层依赖于Usecase 层。

层间通信

Models 层外,每一层都通过接口进行通信。例如,Usecase 层需要 Repository 层,那么它们是如何通信的呢?Repository 层将提供一个接口作为他们的契约和通信。

Repository 层接口示例

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 层接口示例

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 层必须实现这个接口。

测试每层

正如我们所知,简洁意味着独立。每一层都是可测试的,甚至其他层都还不存在。

  • Models 层

    此层仅在任何结构中声明的任何函数/方法时进行测试。

    并且可以独立于其他层进行测试。

  • Repository 层

    为了测试这个层,更好的方法是进行集成测试。但你也可以为每个测试做模拟。我在用 github.com/DATA-DOG/go-sqlmock 作为模拟查询进程msyql的助手。

  • Usecase 层

    因为该层依赖于 Repository 层,意味着该层需要 Repository 层进行测试。所以我们必须基于之前定义的契约接口,做一个 mockery 模拟的 Repository,使用 mockery 进行模拟。

  • Delivery 层

    和用例一样,因为这个层依赖于 Usecase 层,这意味着我们需要用例层进行测试。而用例层也必须基于之前定义的契约接口,用 mockry 进行模拟

对于模拟,我使用 vektragolang 写的 mockery,在这里可以看到 github.com/vektra/mock…

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

}

mockry 将为我生成一个 Repository 层的模型。所以我不需要先完成我的 Repository 层。我可以先完成用例,即使我的 Repository 层还没有实现。

Delivery 层测试

Delivery 层测试将取决于您如何交付数据。如果使用 httprestapi,我们可以在 golang 中使用 httptest 的内置包。

因为这取决于 Usecase 层,所以我们需要一个 Usecase 层的模拟。和 Repository 一样,我还使用 mockry 来模拟我的用例,用于交付测试。

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

您可以看到,每一层都与其依赖项合并为一层。

结论:

简言之,如果画在一个图表中,可以看到下面

  • 这里的每一个库你都可以自己更改。因为简介架构的要点是:不管你的库是什么,但是你的架构是简洁的,而且可测试性也是独立的。

这就是我组织我的项目的方式,你可以争论,或者同意,或者也许改进的更好,只需留下评论和分享

示例项目

示例项目可以在这里看到 github.com/bxcodec/go-…

我的项目中用到的库:

Glide:用于包管理

  • Glide : 包管理
  • go-sqlmock 来自 github.com/DATA-DOG/go-sqlmock
  • Testify : 测试
  • Echo Labstack (Golang Web Framework) : Delivery 层
  • Viper : 环境配置

关于简洁架构的进一步阅读:

本文第二部分:

  • hackernoon.com/trying-clea…

  • 8thlight.com/blog/uncle-…

  • 简洁架构的另一个版本:

    manuel.kiessling.net/2012/09/28/…

如果你有问题,或者需要更多的解释,或者一些我在这里不能很好解释的事情,你可以通过我的 linkedin 问我或者给我发邮件。

linkedin: www.linkedin.com/in/imantumo…

email: iman.tumorang@gmail.com

谢谢你

原文连接:medium.com/hackernoon/…


推荐阅读
  • 深入解析轻量级数据库 SQL Server Express LocalDB
    本文详细介绍了 SQL Server Express LocalDB,这是一种轻量级的本地 T-SQL 数据库解决方案,特别适合开发环境使用。文章还探讨了 LocalDB 与其他轻量级数据库的对比,并提供了安装和连接 LocalDB 的步骤。 ... [详细]
  • 本文详细解析 Skynet 的启动流程,包括配置文件的读取、环境变量的设置、主要线程的启动(如 timer、socket、monitor 和 worker 线程),以及消息队列的实现机制。 ... [详细]
  • 本文介绍了基于Java的在线办公工作流系统的毕业设计方案,涵盖了MyBatis框架的应用、源代码分析、调试与部署流程、数据库设计以及相关论文撰写指导。 ... [详细]
  • 本文探讨了在SharePoint环境中使用BDC(Business Data Catalog)时遇到的问题及其解决策略,包括XML文件导入SSP后的不可见性问题以及与远程SQL Server 2005连接的难题。 ... [详细]
  • Git版本控制基础解析
    本文探讨了Git作为版本控制工具的基本概念及其重要性,不仅限于代码管理,还包括文件的历史记录与版本切换功能。通过对比Git与SVN,进一步阐述了分布式版本控制系统的独特优势。 ... [详细]
  • 本文详细介绍了PHP中的几种超全局变量,包括$GLOBAL、$_SERVER、$_POST、$_GET等,并探讨了AJAX的工作原理及其优缺点。通过具体示例,帮助读者更好地理解和应用这些技术。 ... [详细]
  • 本文详细介绍了在PHP中如何获取和处理HTTP头部信息,包括通过cURL获取请求头信息、使用header函数发送响应头以及获取客户端HTTP头部的方法。同时,还探讨了PHP中$_SERVER变量的使用,以获取客户端和服务器的相关信息。 ... [详细]
  • 初探Hadoop:第一章概览
    本文深入探讨了《Hadoop》第一章的内容,重点介绍了Hadoop的基本概念及其如何解决大数据处理中的关键挑战。 ... [详细]
  • 本文详细介绍了如何在PHP中使用Memcached进行数据缓存,包括服务器连接、数据操作、高级功能等。 ... [详细]
  • 本文由公众号【数智物语】(ID: decision_engine)发布,关注获取更多干货。文章探讨了从数据收集到清洗、建模及可视化的全过程,介绍了41款实用工具,旨在帮助数据科学家和分析师提升工作效率。 ... [详细]
  • 如何高效学习鸿蒙操作系统:开发者指南
    本文探讨了开发者如何更有效地学习鸿蒙操作系统,提供了来自行业专家的建议,包括系统化学习方法、职业规划建议以及具体的开发技巧。 ... [详细]
  • 本文提供了多个关键点来帮助开发者提高Java编程能力,包括代码规范、性能优化和最佳实践等方面,旨在指导读者成为更加优秀的Java程序员。 ... [详细]
  • 一键LNMP配置SSL证书实现全站HTTPS访问
    许多网站搭建者选择了便捷的一键LNMP安装包,但在网站部署完成后,配置SSL证书以支持HTTPS访问是一个不可或缺的步骤。本文将详细介绍如何通过简单的步骤完成这一过程。 ... [详细]
  • 高效产品原型设计:技巧、经验和注意事项
    本文由PMTalk社区资深作者AllenDan撰写,分享了如何在日常产品工作中快速有效地设计产品原型,并确保设计易于理解,减少评审时的质疑。文章不仅提供了实用的技巧和经验,还强调了设计过程中的注意事项。 ... [详细]
  • 本文介绍了一种在 Android 开发中动态修改 strings.xml 文件中字符串值的有效方法。通过使用占位符,开发者可以在运行时根据需要填充具体的值,从而提高应用的灵活性和可维护性。 ... [详细]
author-avatar
卿为倾峰888
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有