我使用Go和gRPC建立了一个微服务,并将程序设计和编程的最佳实践应用于该项目。 我写了一系列关于在项目工做中作出的设计决策和取舍的文章,此篇是关于程序设计。html
程序的设计遵循清晰架构(Clean Architecture)¹。 业务逻辑代码分三层:用例(usecase),域模型(model)和数据服务(dataservice)。git
有三个顶级包“usecase”,“model”和“dataservice”,每层一个。 在每一个顶级包(模型除外)中只有一个以该包命名的文件。 该文件为每一个包定义了外部世界的接口。 从顶层向下的依赖结构层次是:“usecase”,“dataservice”和“model”。 上层包依赖于较低层的包,依赖关系永远不会反向。github
“usecase”是应用程序的入口点,本项目大部分业务逻辑都在用例层。 我从这篇文章²中得到了部分业务逻辑思路。 有三个用例“registration”,“listUser”和“listCourse”。 每一个用例都实现了一个业务功能。 用例可能与真实世界的用例不一样,它们的建立是为了说明设计理念。 如下是注册用例的接口:golang
// 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”函数将经过此接口调用“用例”,该接口仅依赖于模型层。sql
如下是“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”是否有效,而后检查用户是否还没有注册,最后调用数据服务层注册用户。编程
此层中的代码负责直接数据库访问。 这是域模型“User”的数据持久层的接口。json
// 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 }
若是须要支持不一样的数据库,则每一个数据库都须要一个单独的实现。 我将在另外一篇文章“事务管理³中会详细解释。数据结构
模型是惟一没有接口的程序层。 在Clean Architecture中,它被称为“实体(Entity)”。 这是我偏离清晰架构的地方。 此应用程序中的模型层没有太多业务逻辑,它只定义数据。 大多数业务逻辑都在“用例”层中。 根据个人经验,因为延迟加载或其余缘由,在执行用例时,大多数状况下域模型中的数据未彻底加载,所以“用例”须要调用数据服务 从数据库加载数据。 因为域模型不能调用数据服务,所以业务逻辑必须是在“用例”层。
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”依赖项。
你可能会问为何要将校验逻辑放在域模型层中,而将业务逻辑放在“用例”层中?由于业务逻辑一般涉及多个域模型或一个模型的多个实例。例如,产品价格的计算取决于购买数量以及商品是否在甩卖,所以必须在“用例”层中。另外一方面,校验逻辑一般依赖于模型的一个实例,所以能够将其放入模型中。若是校验涉及多个模型或模型的多个实例(例如检查用户是否重复注册),则将其放在“用例”层中。
这是我没有遵循清晰架构(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。
[3] Go Microservice with Clean Architecture: Transaction Support
[5] Input validation in GoLang
不堆砌术语,不罗列架构,不迷信权威,不盲从流行,坚持独立思考