我使用Go和gRPC创建了一个微服务,并将程序设计和编程的最佳实践应用于该项目。 我写了一系列关于在项目工作中做出的设计决策和取舍的文章,此篇是关于程序设计。

程序的设计遵循清晰架构(Clean Architecture)¹。 业务逻辑代码分三层:用例(usecase),域模型(model)和数据服务(dataservice)。

有三个顶级包“usecase”,“model”和“dataservice”,每层一个。 在每个顶级包(模型除外)中只有一个以该包命名的文件。 该文件为每个包定义了外部世界的接口。 从顶层向下的依赖结构层次是:“usecase”,“dataservice”和“model”。 上层包依赖于较低层的包,依赖关系永远不会反向。

用例(usecase):

“usecase”是应用程序的入口点,本项目大部分业务逻辑都在用例层。 我从这篇文章²中获得了部分业务逻辑思路。 有三个用例“registration”,“listUser”和“listCourse”。 每个用例都实现了一个业务功能。 用例可能与真实世界的用例不同,它们的创建是为了说明设计理念。 以下是注册用例的接口:


// RegistrationUseCaseInterface is for users to register themselves to an application. It has registration related functions.
// ModifyAndUnregisterWithTx() is the one supporting transaction, the other are not.
type RegistrationUseCaseInterface interface {
// RegisterUser register a user to an application, basically save it to a database. The returned resultUser that has
// a Id ( auto generated by database) after persisted
RegisterUser(user *model.User) (resultUser *model.User, err error)
// UnregisterUser unregister a user from an application by user name, basically removing it from a database.
UnregisterUser(username string) error
// ModifyUser change user information based on the User.Id passed in.
ModifyUser(user *model.User) error
// ModifyAndUnregister change user information and then unregister the user based on the User.Id passed in.
// It is created to illustrate transaction, no real use.
ModifyAndUnregister(user *model.User) error
// ModifyAndUnregisterWithTx change user information and then unregister the user based on the User.Id passed in.
// It supports transaction
// It is created to illustrate transaction, no real use.
ModifyAndUnregisterWithTx(user *model.User) error
// EnableTx enable transaction support on use case. Need to be included for each use case needs transaction
// It replaces the underline database handler to sql.Tx for each data service that used by this use case
EnableTxer
}

“main”函数将通过此接口调用“用例”,该接口仅依赖于模型层。

以下是“registration.go”的部分代码,它实现了“RegistrationUseCaseInterface”中的功能。 “RegistrationUseCase”是具体的结构。 它有两个成员“UserDataInterface”和“TxDataInterface”。 “UserDataInterface”可用于调用数据服务层中的方法(例如“UserDataInterface.Insert(user)”)。 “TxDataInterface”用于实现事务。 它们的具体类型由应用程序容器(ApplicationContainer)创建,并通过依赖注入到每个函数中。 任何用例代码仅依赖于数据服务接口,并不依赖于数据库相关代码(例如,sql.DB或sql.Stmt)。 任何数据库访问代码都通过数据服务接口执行。

// RegistrationUseCase implements RegistrationUseCaseInterface.
// It has UserDataInterface, which can be used to access persistence layer
// TxDataInterface is needed to support transaction
type RegistrationUseCase struct {
UserDataInterface dataservice.UserDataInterface
TxDataInterface dataservice.TxDataInterface
} func (ruc *RegistrationUseCase) RegisterUser(user *model.User) (*model.User, error) {
err := user.Validate()
if err != nil {
return nil, errors.Wrap(err, "user validation failed")
}
isDup, err := ruc.isDuplicate(user.Name)
if err != nil {
return nil, errors.Wrap(err, "")
}
if isDup {
return nil, errors.New("duplicate user for " + user.Name)
}
resultUser, err := ruc.UserDataInterface.Insert(user) if err != nil {
return nil, errors.Wrap(err, "")
}
return resultUser, nil
}

通常一个用例可以具有一个或多个功能。 上面的代码显示了“RegisterUser”功能。 它首先检查传入的参数“user”是否有效,然后检查用户是否尚未注册,最后调用数据服务层注册用户。

数据服务(Data service):

此层中的代码负责直接数据库访问。 这是域模型“User”的数据持久层的接口。

// UserDataInterface represents interface for user data access through database
type UserDataInterface interface {
// Remove deletes a user by user name from database.
Remove(username string) (rowsAffected int64, err error)
// Find retrieves a user from database based on a user's id
Find(id int) (*model.User, error)
// FindByName retrieves a user from database by User.Name
FindByName(name string) (user *model.User, err error)
// FindAll retrieves all users from database as an array of user
FindAll() ([]model.User, error)
// Update changes user information on the User.Id passed in.
Update(user *model.User) (rowsAffected int64, err error)
// Insert adds a user to a database. The returned resultUser has a Id, which is auto generated by database
Insert(user *model.User) (resultUser *model.User, err error)
// Need to add this for transaction support
EnableTxer
}

以下是“UserDataInterface”中MySql实现“insert”功能的代码。 这里我使用“gdbc.SqlGdbc”接口作为数据库处理程序的封装以支持事务。 “gdbc.SqlGdbc”接口的具体实现可以是sql.DB(不支持事务)或sql.Tx(支持事务)。 通过“UserDataSql”结构传入函数作为接收者,使“Insert()”函数对事务变得透明。 在“insert”函数中,它首先从“UserDataSql”获取数据库链接,然后创建预处理语句(Prepared statement)并执行它; 最后它获取插入的id并将其返回给调用函数。

// UserDataSql is the SQL implementation of UserDataInterface
type UserDataSql struct {
DB gdbc.SqlGdbc
} func (uds *UserDataSql) Insert(user *model.User) (*model.User, error) { stmt, err := uds.DB.Prepare(INSERT_USER)
if err != nil {
return nil, errors.Wrap(err, "")
}
defer stmt.Close()
res, err := stmt.Exec(user.Name, user.Department, user.Created)
if err != nil {
return nil, errors.Wrap(err, "")
}
id, err := res.LastInsertId()
if err != nil {
return nil, errors.Wrap(err, "")
}
user.Id = int(id)
logger.Log.Debug("user inserted:", user)
return user, nil
}

如果需要支持不同的数据库,则每个数据库都需要一个单独的实现。 我将在另一篇文章“事务管理³中会详细解释。

域模型(Model):

模型是唯一没有接口的程序层。 在Clean Architecture中,它被称为“实体(Entity)”。 这是我偏离清晰架构的地方。 此应用程序中的模型层没有太多业务逻辑,它只定义数据。 大多数业务逻辑都在“用例”层中。 根据我的经验,由于延迟加载或其他原因,在执行用例时,大多数情况下域模型中的数据未完全加载,因此“用例”需要调用数据服务 从数据库加载数据。 由于域模型不能调用数据服务,因此业务逻辑必须是在“用例”层。

数据校验(Validation):
import (
"github.com/go-ozzo/ozzo-validation"
"time"
) // User has a name, department and created date. Name and created are required, department is optional.
// Id is auto-generated by database after the user is persisted.
// json is for couchdb
type User struct {
Id int `json:"uid"`
Name string `json:"username"`
Department string `json:"department"`
Created time.Time `json:"created"`
} // Validate validates a newly created user, which has not persisted to database yet, so Id is empty
func (u User) Validate() error {
return validation.ValidateStruct(&u,
validation.Field(&u.Name, validation.Required),
validation.Field(&u.Created, validation.Required))
} //ValidatePersisted validate a user that has been persisted to database, basically Id is not empty
func (u User) ValidatePersisted() error {
return validation.ValidateStruct(&u,
validation.Field(&u.Id, validation.Required),
validation.Field(&u.Name, validation.Required),
validation.Field(&u.Created, validation.Required))
}

以上是域模型“User”的代码,其中有简单的数据校验。将校验逻辑放在模型层中是很自然的,模型层应该是应用程序中的最低层,因为其他层都依赖它。校验规则通常只涉及低级别操作,因此不应导致任何依赖问题。此应用程序中使用的校验库是ozzo-validation⁴。它是基于接口的,减少了对代码的干扰。请参阅GoLang中的输入验证⁵来比较不同的校验库。一个问题是“ozzo”依赖于“database/sql”包,因为支持SQL校验,这搞砸了依赖关系。将来如果出现依赖问题,我们可能需要切换到不同的库或删除库中的“sql”依赖项。

你可能会问为什么要将校验逻辑放在域模型层中,而将业务逻辑放在“用例”层中?因为业务逻辑通常涉及多个域模型或一个模型的多个实例。例如,产品价格的计算取决于购买数量以及商品是否在甩卖,因此必须在“用例”层中。另一方面,校验逻辑通常依赖于模型的一个实例,因此可以将其放入模型中。如果校验涉及多个模型或模型的多个实例(例如检查用户是否重复注册),则将其放在“用例”层中。

数据传输对象(DTO)

这是我没有遵循清晰架构(Clean Architecture)的另一项。 根据清晰架构(Clean Architecture)¹,“通常跨越边界的数据是简单的数据结构。 如果你愿意,可以使用基本结构或简单的数据传输对象(DTO)。“在本程序中不使用DTO(数据传输对象),而使用域模型进行跨越边界的数据传输。 如果业务逻辑非常复杂,那么拥有一个单独的DTO可能会有一些好处,那时我不介意创建它们,但现在不需要。

格式转换

跨越服务边界时,我们确实需要拥有不同的域模型。 例如本应用程序也作为gRPC微服务发布。 在服务器端,我们使用本程序域模型; 在客户端,我们使用gRPC域模型,它们的类型是不同的,因此需要进行格式转换。

// GrpcToUser converts from grpc User type to domain Model user type
func GrpcToUser(user *uspb.User) (*model.User, error) {
if user == nil {
return nil, nil
}
resultUser := model.User{} resultUser.Id = int(user.Id)
resultUser.Name = user.Name
resultUser.Department = user.Department
created, err := ptypes.Timestamp(user.Created)
if err != nil {
return nil, errors.Wrap(err, "")
}
resultUser.Created = created
return &resultUser, nil
} // UserToGrpc converts from domain Model User type to grpc user type
func UserToGrpc(user *model.User) (*uspb.User, error) {
if user == nil {
return nil, nil
}
resultUser := uspb.User{}
resultUser.Id = int32(user.Id)
resultUser.Name = user.Name
resultUser.Department = user.Department
created, err := ptypes.TimestampProto(user.Created)
if err != nil {
return nil, errors.Wrap(err, "")
}
resultUser.Created = created
return &resultUser, nil
} // UserListToGrpc converts from array of domain Model User type to array of grpc user type
func UserListToGrpc(ul []model.User) ([]*uspb.User, error) {
var gul []*uspb.User
for _, user := range ul {
gu, err := UserToGrpc(&user)
if err != nil {
return nil, errors.Wrap(err, "")
}
gul = append(gul, gu)
}
return gul, nil
}

上述数据转换代码位于“adapter/userclient”包中。 乍一看,似乎应该让域模型“User”具有方法“toGrpc()”,它将像这样执行 - “user.toGrpc(user * uspb.User)”,但这将使业务域模型依赖于gRPC。 因此,最好创建一个单独的函数并将其放在“adapter/userclient”包中。 该包将依赖于域模型和gRPC模型。 正因为如此,保证了域模型和gRPC模型都是干净的,它们并不相互依赖。

结论:

本应用程序的设计遵循清晰架构(Clean Architecture)。 业务逻辑代码有三层:“用例”,“域模型”和“数据服务”。 但是我在两个方面偏离了清晰架构(Clean Architecture)。 一个是我把大多数业务逻辑代码放在“用例”层; 另一个是我没有数据传输对象(DTO),而是使用域模型在不同层之间进行共享数据。

源程序:

完整的源程序链接 github

索引:

[1]The Clean Code Blog

[2]Clean Architecture in Go

[3] Go Microservice with Clean Architecture: Transaction Support

[4]ozzo-validation

[5] Input validation in GoLang

清晰架构(Clean Architecture)的Go微服务: 程序设计的更多相关文章

  1. (转)微服务架构 互联网保险O2O平台微服务架构设计

    http://www.cnblogs.com/Leo_wl/p/5049722.html 微服务架构 互联网保险O2O平台微服务架构设计 关于架构,笔者认为并不是越复杂越好,而是相反,简单就是硬道理也 ...

  2. Java 18套JAVA企业级大型项目实战分布式架构高并发高可用微服务电商项目实战架构

    Java 开发环境:idea https://www.jianshu.com/p/7a824fea1ce7 从无到有构建大型电商微服务架构三个阶段SpringBoot+SpringCloud+Solr ...

  3. 通俗地理解面向服务的架构(SOA)以及微服务之间的关系

    SOA是一种软件的应用架构方法,它基于面向对象,但又不是面向对象,整体上是面向服务的架构.SOA由精确的服务定义.松散的构件服务组成,以及业务流程调用等多个方面形成的一整套架构方法. 这话是不是听起来 ...

  4. Re:从0开始的微服务架构--(二)快速快速体验微服务架构?--转

    原文地址:https://mp.weixin.qq.com/s/QO1QDQWnjHZp8EvGDrxZvw 这是专题的第二篇文章,看看如何搭建一个简单模式的微服务架构. 记得好久之前看到一个大牛说过 ...

  5. 【分布式微服务企业快速架构】SpringCloud分布式、微服务、云架构快速开发平台源码

    鸿鹄云架构[系统管理平台]是一个大型 企业.分布式.微服务.云架构的JavaEE体系快速研发平台,基于 模块化.微服务化.原子化.热部署的设计思想,使用成熟领先的无商业限制的主流开源技术 (Sprin ...

  6. 清晰架构(Clean Architecture)的Go微服务: 设计原则

    我最近写了一个Go微服务应用程序,这个程序的设计来自三个灵感: 清晰架构"Clean Architecture"¹ and SOLID (面向对象设计)² 设计 原则³ Sprin ...

  7. 清晰架构(Clean Architecture)的Go微服务

    我用Go和gRPC创建了一个微服务项目,并试图找出最好的程序结构,它可以作为我其他项目的模板.我还将程序设计和编程的最佳实践应用于Go Microservice程序,例如清晰架构(Clean Arch ...

  8. 微服务架构-选择Spring Cloud,放弃Dubbo

    Spring Cloud 在国内中小型公司能用起来吗?从 2016 年初一直到现在,我们在这条路上已经走了一年多. 在使用 Spring Cloud 之前,我们对微服务实践是没有太多的体会和经验的.从 ...

  9. dubbo和dubboX与微服务架构(dubbo一)

    一.传统三层架构模式的缺陷 三层架构(3-tier architecture) 通常意义上的三层架构就是将整个业务应用划分为:界面层(User Interface layer)web.业务逻辑层(Bu ...

随机推荐

  1. 【Python】循环控制保留字

  2. Centos7在防火墙中添加访问端口

    1.       查看jenkins启动状态命令:systemctl status Jenkins 保证jenkins启动,此处的状态为正在运行 2.       查看防火墙状态命令:systemct ...

  3. Book: The TimeViz Browser

    website; A visual survey of visualization techniques for time-oriented data. 1. Left pannel ----- fi ...

  4. C语言二维数组指针与指针数组

    http://c.biancheng.net/view/2022.html http://c.biancheng.net/view/2020.html

  5. SpringBoot整合WEB开发--(四)@ControllerAdvice

    1.全局异常处理: @ControllerAdvice处理全局数据,一般搭配@ExceptionHandler,@ModelAttribute以及@InitBinder使用. @ControllerA ...

  6. ASP.NET + MVC5 入门完整教程七 -—-- MVC基本工具(上)

    https://blog.csdn.net/qq_21419015/article/details/80474956 这里主要介绍三类工具之一的 依赖项注入(DI)容器,其他两类 单元测试框架和模仿工 ...

  7. Linux - cron - 基础

    概述 cron 相关的理解与使用 背景 最近实在没啥写的了 我写东西, 一般是是这些 看了书过后, 做一些系统的整理 比如之前的 docker 和 git 系列 遇到了实际问题, 解决过程也不是那么顺 ...

  8. Django_Setings

    """ Django settings for untitled1 project. Generated by 'django-admin startproject' u ...

  9. webpack配置的说明

    {devtool: 'source-map',//要启用source-map需加上此配置项,同时css或less的loader要加上参数?sourceMap,js的loader不用加 entry: e ...

  10. Python记:通用的序列操作之成员资格(听起来倒是有些抽象的!)

    ______________________________永远守护这一尘不染的真心! 要检查特定的值是否包含在序列中,可使用运算符in.它检查是否满足指定的条件,并返回相应的值:满足时返回True, ...