参数验证如何遵循第一法则DRY?

14 阅读3分钟
转存失败,建议直接上传图片文件

案例:

如上图为常见的网站登陆场景,用户第一步输入手机号,点击获取短信验证码;第二步输入手机收到的短信验证码,点击登录按钮完成登录。

发送验证码接口:
判断手机号非空,手机号数字型,手机号格式是否正确

登录接口:
判断手机号非空,手机号数字型,手机号格式是否正确
判断验证码非空,验证码数字型,验证码位数是否正确

V1版本:

type SmsCaptchaV1 struct {
    Mobile string `binding:"required,number,mobile"`
}

type UserLoginV1 struct {
    Mobile  string `binding:"required,number,mobile"`
    Captcha string `binding:"required,number,len=4"`
}

我们使用了gin自带的参数校验框架实现了手机和验证码校验需求,这里有什么问题吗?

Mobile的校验规则重复违反了DRY原则,当需要修改Mobile校验规则的时候,需要修改多个地方,就这是代码坏味道中的霰弹式修改。

那如何改进呢,我们来到了V2版本:

type MobileFieldV2 struct {
    Mobile string `binding:"required,number,mobile"`
}

type CaptchaFieldV2 struct {
    Captcha string `binding:"required,number,len=4"`
}

type SmsCaptchaV2 struct {
    MobileFieldV2
}

type UserLoginV2 struct {
    MobileFieldV2
    CaptchaFieldV2
}

我们通过把Mobile、Captcha抽取出来,独立成两个校验小对象,把校验规则包含在这个小对象中,然后通过组合的方式继承了小对象中的校验规则,从而达到了重用的目的。

开发中参数校验分为两种:
1、必须验证,如上面两个接口
2、调用方传了才验证,不传不验证,比如常见的查询接口

V2版本的required标签导致所有包含校验小对象都必须验证参数,不能灵活支持上述两种校验情况,所以我们来到了最终版本:

type MobileField struct {
    Mobile string `binding:"number,mobile"`
}

type CaptchaField struct {
    Captcha string `binding:"number,len=4"`
}

type SmsCaptcha struct {
    MobileField `binding:"required"`
}

type UserLogin struct {
    MobileField `binding:"required"`
    CaptchaField `binding:"required"`
}

我们把required标签提取到SmsCaptcha、UserLogin结构体中,由使用方灵活设置校验条件。
1、binding:"required" 必须验证
2、binding:"omitempty" 调用方传了才验证,不传不验证

type UserMany struct {
    MobileField        `binding:"omitempty"`
    user.AgeField      `binding:"omitempty"`
    user.LevelField    `binding:"omitempty"`
    user.NicknameField `binding:"omitempty"`
}

gin自带的校验框架默认required,所以必须验证的情况可以省略required标签。

为什么AgeField、LevelField、NicknameField放在user目录中?
比如Level字段,有可能出现在多个表中,商家表中的Level字段很有可能与用户表中的Level字段校验规则不一致,所以分目录区分。

这个版本可以很从容的解决日常开发中的校验需求,避免了霰弹式修改,提高了代码内聚性。

那还存在什么问题呢?

当服务器需要支持gRPC时,protobuf配置手机参数校验时又导致了校验规则重复:

message UserLogin {
  string Mobile = 1 [(buf.validate.field).string.len = 11, (buf.validate.field).string.pattern = "^(1[3-9][0-9]\d{8})$"];
  string Captcha = 2 [(buf.validate.field).string.len = 4, (buf.validate.field).string.pattern = "^[0-9]*$"];
}

寻根溯源,为什么会出现这种情况,参数校验逻辑应该放在哪里合适,请看下回分解。

源码链接