Golang Model 字段自动化校验设计
背景
在我们日常开发中,不可避免的总要去进行各种参数校验,但是如果在某个场景中,要校验的字段非常多,并且在其中还有耦合关系,那么我们手写校验逻辑就变得非常的低效且难以维护。本篇文档就基于 DDD 领域模型设计的思想下,提供自动化的校验模型字段。
常见的字段校验方式
数据校验在业务逻辑代码中有着至关重要的作用,关系到整个后续业务是否可以正常运行。对参数的校验根据其具体业务逻辑与场景,可以分为字段校验、依赖校验、功能校验与逻辑校验四个部分。
字段校验
字段校验是最常见的校验类型。例如:商品名称不能超过多少个字符,商品状态必须是有效等。
func (e *Shop) ValidateShopName() error {
if e.Name != nil && e.Name == "" {
return errors.New("商品名称不能为空。")
}
if e.Name != nil && utf8.RuneCountInString(e.Name) > constant.MaxShopNameLength {
return errors.Errorf("商品名称长度为 %d, 不能超过 %d ", utf8.RuneCountInString(e.Name), constant.MaxShopNameLength)
}
return nil
}
依赖校验
依赖校验,顾名思义是在业务逻辑中依赖了其他模块。例如,在创建商品信息时,要校验一下商品依赖的商家或供应商等信息是否合法。
func (e *Shop) ValidateMerchant() error {
// 在此方法中可能需要进行外部调用或者查询 DB 的操作。
if e.HasInvalidMerchant() {
return errors.New("商家信息存在异常")
}
return nil
}
功能校验
功能校验例如用户是否有权限发布商品、商品信息是否与其他商品存在冲突等。
func (e *Shop) ValidateUserPermission() error {
if e.UserCreateShopWithoutPermission() {
return errors.New("用户无权限创建商品")
}
return nil
}
逻辑校验
逻辑校验主要是一些具体的业务逻辑。例如在下架商品时,校验是否有新用户下单等。
func (e *Shop) ValidateCloseShop() error{
if e.InvalidShopStatus() {
return errors.New("商品已下架")
}
if e.ExistShopTicket() {
return errors.New("有正在进行的订单信息,无法下架")
}
return nil
}
上面我们列出来常见的四种校验方式,当我们在一个复杂且庞大的业务场景需要把各种各样的校验放在一起去校验时,我们不得不编写一个庞大的校验函数,把这些单点的校验函数聚合起来,更有甚者都没有进行子逻辑校验的函数区分,就是第一个大函数,把各种各样的校验逻辑代码写到一个函数中,那么长此以往,校验逻辑就会非常复杂,无法迭代。
func (e *Shop) ValidateCreateShop() error {
if err = e.ValidateShopName(); err != nil {
return err
}
if err = e.ValidateDescrption(); err != nil {
return err
}
if err = e.ValidateImage(); err != nil {
return err
}
if err = e.ValidateMerchant(); err != nil {
return err
}
if err = e.ValidateUserPermission(); err != nil {
return err
}
if err = e.ValidateCloseShop(); err != nil {
return err
}
return nil
}
自动化校验
type Validator struct {
FieldNames []string // 需要更新的字段
ValidateNames []string // 需要校验的字段列表
ValidateFuncList []Func() error // 校验函数列表
}
func (v *Validator) Validate() error {
for _, validate := range v.validateFuncList {
if err := validate(); err != nil {
return err
}
}
return nil
}
// GetFields2ValidateFuncMap 各个字段的校验函数在这里扩展,在调用 register 函数时,会自动注册
func (a *Aggregate) GetFields2ValidateFuncMap() map[string]func() error {
return map[string]func() error {
constant.ShopForCreate: a.Shop.ValidateCreateShop,
constant.ShopForUpdate: a.Shop.ValidateUpdateShop,
constant.ShopCanStart: a.Shop.CanStart,
// ... 等等各种校验都可以在这里定义一个聚合函数列表
}
}
func DTOToAgg(dto *DTO.Shop) (*shop.Aggregate, error) {
baseShop := base.NewBaseShop()
// 先把传参 model 转化成领域数据
if err = copier.Copy(baseShop, dto); err != nil {
return nil, errors.Wrap(err, err.Error())
}
// New 一个聚合类
shopAgg := shop.NewShopAggregate(baseShop)
// 获取本次传给领域对象的字段,以及加载要校验的字段
setFields := GetSetOptionalFields(*dto)
var validateName []string
for _, field := range setFields {
validateName = append(validateName, field)
}
shopAgg.SetUpdateFields(setFields)
// 注册 validate 函数
shopAgg.RegisterValidator(validateName)
return shopAgg, nil
}
// 执行校验函数
func (v *Validator) ValidateMultipleFields(ctx context.Context) error {
for _, validate := range v.validateFuncList {
if err := validate(); err != nil {
return err
}
}
return
}

简单来描述自动校验分为以下几个步骤:
- 在接收传参的转换函数中,先把本次请求传入的字段拿到,并且注册这些字段对应的校验函数。
- 进入到业务逻辑处理的函数中,再次增加一些当前业务场景需要的特殊校验函数。
- 依次执行校验函数,观察是否有报错。