热门标签 | 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/…


推荐阅读
  • 从壹开始前后端分离【 .NET Core2.0 +Vue2.0 】框架之六 || API项目整体搭建 6.1 仓储模式
    代码已上传Github+Gitee,文末有地址  书接上文:前几回文章中,我们花了三天的时间简单了解了下接口文档Swagger框架,已经完全解放了我们的以前的Word说明文档,并且可以在线进行调 ... [详细]
  • 基于PgpoolII的PostgreSQL集群安装与配置教程
    本文介绍了基于PgpoolII的PostgreSQL集群的安装与配置教程。Pgpool-II是一个位于PostgreSQL服务器和PostgreSQL数据库客户端之间的中间件,提供了连接池、复制、负载均衡、缓存、看门狗、限制链接等功能,可以用于搭建高可用的PostgreSQL集群。文章详细介绍了通过yum安装Pgpool-II的步骤,并提供了相关的官方参考地址。 ... [详细]
  • [译]技术公司十年经验的职场生涯回顾
    本文是一位在技术公司工作十年的职场人士对自己职业生涯的总结回顾。她的职业规划与众不同,令人深思又有趣。其中涉及到的内容有机器学习、创新创业以及引用了女性主义者在TED演讲中的部分讲义。文章表达了对职业生涯的愿望和希望,认为人类有能力不断改善自己。 ... [详细]
  • CentOS 7部署KVM虚拟化环境之一架构介绍
    本文介绍了CentOS 7部署KVM虚拟化环境的架构,详细解释了虚拟化技术的概念和原理,包括全虚拟化和半虚拟化。同时介绍了虚拟机的概念和虚拟化软件的作用。 ... [详细]
  • Oracle优化新常态的五大禁止及其性能隐患
    本文介绍了Oracle优化新常态中的五大禁止措施,包括禁止外键、禁止视图、禁止触发器、禁止存储过程和禁止JOB,并分析了这些禁止措施可能带来的性能隐患。文章还讨论了这些禁止措施在C/S架构和B/S架构中的不同应用情况,并提出了解决方案。 ... [详细]
  • 云原生应用最佳开发实践之十二原则(12factor)
    目录简介一、基准代码二、依赖三、配置四、后端配置五、构建、发布、运行六、进程七、端口绑定八、并发九、易处理十、开发与线上环境等价十一、日志十二、进程管理当 ... [详细]
  • 本文详细介绍了SQL日志收缩的方法,包括截断日志和删除不需要的旧日志记录。通过备份日志和使用DBCC SHRINKFILE命令可以实现日志的收缩。同时,还介绍了截断日志的原理和注意事项,包括不能截断事务日志的活动部分和MinLSN的确定方法。通过本文的方法,可以有效减小逻辑日志的大小,提高数据库的性能。 ... [详细]
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • 如何用UE4制作2D游戏文档——计算篇
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了如何用UE4制作2D游戏文档——计算篇相关的知识,希望对你有一定的参考价值。 ... [详细]
  • Oracle分析函数first_value()和last_value()的用法及原理
    本文介绍了Oracle分析函数first_value()和last_value()的用法和原理,以及在查询销售记录日期和部门中的应用。通过示例和解释,详细说明了first_value()和last_value()的功能和不同之处。同时,对于last_value()的结果出现不一样的情况进行了解释,并提供了理解last_value()默认统计范围的方法。该文对于使用Oracle分析函数的开发人员和数据库管理员具有参考价值。 ... [详细]
  • 深度学习中的Vision Transformer (ViT)详解
    本文详细介绍了深度学习中的Vision Transformer (ViT)方法。首先介绍了相关工作和ViT的基本原理,包括图像块嵌入、可学习的嵌入、位置嵌入和Transformer编码器等。接着讨论了ViT的张量维度变化、归纳偏置与混合架构、微调及更高分辨率等方面。最后给出了实验结果和相关代码的链接。本文的研究表明,对于CV任务,直接应用纯Transformer架构于图像块序列是可行的,无需依赖于卷积网络。 ... [详细]
  • 如何利用 Myflash 解析 binlog ?
    本文主要介绍了对Myflash的测试,从准备测试环境到利用Myflash解析binl ... [详细]
  • Oracle :修改数据库服务器字符集 ... [详细]
  • 2016 linux发行版排行_灵越7590 安装 linux (manjarognome)
    RT之前做了一次灵越7590黑苹果炒作业的文章,希望能够分享给更多不想折腾的人。kawauso:教你如何给灵越7590黑苹果抄作业​zhuanlan.z ... [详细]
  • MySQL数据库锁机制及其应用(数据库锁的概念)
    本文介绍了MySQL数据库锁机制及其应用。数据库锁是计算机协调多个进程或线程并发访问某一资源的机制,在数据库中,数据是一种供许多用户共享的资源,如何保证数据并发访问的一致性和有效性是数据库必须解决的问题。MySQL的锁机制相对简单,不同的存储引擎支持不同的锁机制,主要包括表级锁、行级锁和页面锁。本文详细介绍了MySQL表级锁的锁模式和特点,以及行级锁和页面锁的特点和应用场景。同时还讨论了锁冲突对数据库并发访问性能的影响。 ... [详细]
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社区 版权所有