一种优雅解决go语言if if场景的方式方法

559 阅读5分钟

写在前面

我们在Go语言中, 经常会用到使用if的场景, 比如网络上关于go if的表情包

image.png

go语言应该是我所用过的语言中, 使用if最频繁的语言了吧, 甚至没有之一. 那么我们在众多if的海洋里, 又该如何优雅遨游呢?

日常if写法

    type User struct {  
    Name string `json:"name"`  
    Age int `json:"age"`  
    Phone string `json:"phone"`  
    Email string `json:"email"`  
    Remark string `json:"remark"`  
}  
  
func checkUser(user *User) error {  
    if user == nil {
        return errors.New("用户不能为空")
    }
    
    if user.Age < 18 {  
        return errors.New("年龄太小")  
    }  
  
    if user.Age > 100 {  
        return errors.New("年龄太大")  
    }  

    if user.Phone == "" {  
        return errors.New("手机号不能为空")  
    }  

    if user.Email == "" {  
        return errors.New("邮箱不能为空")  
    }  

    return nil  
}

这里对User对象的各个字段进行业务校验, 可以发现, 随着校验的逻辑增加, checkUser方法if逐渐多了起来, 如果我们再增加一些逻辑呢

func checkUser(user *User) error {  
    if user == nil {  
        return errors.New("用户不能为空")  
    }  

    if user.Age < 18 {  
        return errors.New("年龄太小")  
    }  

    if user.Age > 100 {  
        return errors.New("年龄太大")  
    }  

    if user.Phone == "" {  
        return errors.New("手机号不能为空")  
    }  

    if user.Email == "" {  
        return errors.New("邮箱不能为空")  
    }  

    if user.Remark == "" {  
        return errors.New("备注不能为空")  
    }  

    if user.Name == "" {  
        return errors.New("姓名不能为空")  
    }  

    if utf8.RuneCountInString(user.Name) > 10 {  
        return errors.New("姓名不能超过10个字")  
    }  

    if utf8.RuneCountInString(user.Remark) > 100 {  
        return errors.New("备注不能超过100个字")  
    }  

    return nil  
}

可以发现, 随着校验逻辑的增加, if页逐渐变得臃肿起来, 如果我们这每个if里面再包含一些db操作等, 逻辑是不是更复杂了呢? 为了解决这一问题, 我们试着把同一个属性的校验放在一个函数里面去试试看.

func checkUser(user *User) error {  
    if user == nil {  
        return errors.New("用户不能为空")  
    }  

    if err := checkName(user.Name); err != nil {  
        return err  
    }  

    if err := checkAge(user.Age); err != nil {  
        return err  
    }  

    if err := checkPhone(user.Phone); err != nil {  
        return err  
    }  

    if err := checkEmail(user.Email); err != nil {  
        return err  
    }  

    if err := checkRemark(user.Remark); err != nil {  
        return err  
    }  

    return nil  
}  

func checkAge(age int) error {  
    if age < 18 {  
        return errors.New("年龄太小")  
    }  

    if age > 100 {  
        return errors.New("年龄太大")  
    }  

    return nil  
}  

func checkPhone(phone string) error {  
    if phone == "" {  
        return errors.New("手机号不能为空")  
    }  

    return nil  
}  

func checkEmail(email string) error {  
    if email == "" {  
        return errors.New("邮箱不能为空")  
    }  

    return nil  
}  

func checkRemark(remark string) error {  
    if remark == "" {  
        return errors.New("备注不能为空")  
    }  

    if utf8.RuneCountInString(remark) > 100 {  
        return errors.New("备注不能超过100个字")  
    }  

    return nil  
}  

func checkName(name string) error {  
    if name == "" {  
        return errors.New("姓名不能为空")  
    }  

    if utf8.RuneCountInString(name) > 10 {  
        return errors.New("姓名不能超过10个字")  
    }  

    return nil  
}

我们把相同属性的校验都放在了一个共同的函数里面, 逻辑层次更好了, 但是发现, 从某种意义上来说, if还变多了, 这好像也没有达到我们想要的.

接下来, 就要引入本次的主题了, 执行链, 顾名思义, 就是一个执行的链路或者链条

执行链定义

  • 类型定义
type (
    //  Interface传入执行链的类型, 是一个函数, 返回值为error, 当然, 可以根据自己的具体的业务, 定义这个类型
    Interface func() error

    // Chain 执行链类型, 这里只做了简单的封装, 其实我们在具体业务中还可以加入一些属性, 比如校验某个人是否存在, 如果存在, 就把查到的数据存下来, 传递给后面的逻辑用, 这样就形成了一个有状态的执行链
    Chain struct {
        tasks []Interface
    }
    
    // Option 用于提供公共函数, 操作内部属性或方法
    Option func(*Chain)
)
  • New
// NewChain 创建一个执行链实例
func NewChain(opts ...Option) *Chain {
	c := &Chain{}
	for _, opt := range opts {
		opt(c)
	}
	return c
}

// WithTasks 该函数用户像New持续传递执行的任务函数
func WithTasks(chain ...Interface) Option {
    return func(c *Chain) {
        c.tasks = append(c.tasks, chain...)
    }
}

// Do 执行整个链路
func (c *Chain) Do() error {
    for _, task := range c.tasks {
            if err := task(); err != nil {
                    return err
            }
    }
    return nil
}

工具我们封装好了, 那上述的业务逻辑如何改进呢? 且看如下所示

  1. 首先拆分每个校验逻辑, 争取每个都是尽可能小的执行块
  2. 如果不能拆分的, 我们也可以把这一块看作是整体, 然后再把这个整体执行链加入更大的执行链
func checkUser(user *User) error {  
    checkChain := NewChain(  
    WithTasks(  
            checkUserIsNil(user),  
            checkUserAge(user),  
            func() error {  
                return checkName(user.Name)  
            },  
            func() error {  
                return checkPhone(user.Phone)  
            },  
            func() error {  
                return checkEmail(user.Email)  
            },  
            func() error {  
                return checkRemark(user.Remark)  
            },  
        ),  
    )  
  
    return checkChain.Do()  
}  
  
func checkUserIsNil(user *User) Interface {  
    return func() error {  
        if user == nil {  
            return errors.New("用户不能为空")  
        }  
        return nil  
    }  
}  
  
func checkUserAge(user *User) Interface {  
    return func() error {  
        return checkAge(user.Age)  
    }  
}  
  
func checkAge(age int) error {  
    if age < 18 {  
        return errors.New("年龄太小")  
    }  

    if age > 100 {  
        return errors.New("年龄太大")  
    }  

    return nil  
}  
  
func checkPhone(phone string) error {  
    if phone == "" {  
        return errors.New("手机号不能为空")  
    }  

    // 正则表达式校验手机号格式  

    return nil  
}  
  
func checkEmail(email string) error {  
    if email == "" {  
        return errors.New("邮箱不能为空")  
    }  

    // 正则表达式校验邮箱格式  
    // ...  

    return nil  
}  
  
func checkRemark(remark string) error {  
    if remark == "" {  
        return errors.New("备注不能为空")  
    }  

    if utf8.RuneCountInString(remark) > 100 {  
        return errors.New("备注不能超过100个字")  
    }  

    if utf8.RuneCountInString(remark) < 10 {  
        return errors.New("备注不能少于10个字")  
    }  

    return nil  
}  
  
func checkName(name string) error {  
    if name == "" {  
        return errors.New("姓名不能为空")  
    }  

    if utf8.RuneCountInString(name) > 10 {  
        return errors.New("姓名不能超过10个字")  
    }  

    if utf8.RuneCountInString(name) < 2 {  
        return errors.New("姓名不能少于2个字")  
    }  

    return nil  
}

可以看到, if得到了缓解, 但是, 到这里, 有的同学可能就有疑问了, 我就写了七八个if, 被你这么一改, 代码还变多了, 我干嘛要这么写呢? 我需求还做不完呢!!!

为什么要这么做?

先来回顾一下上述, 我们总共做了两件事

  1. 拆分校验的业务逻辑, 把校验变成最小单元(其实这里还可以细分到具体的规则里面, 由于是举例说明, 就不再细分了), 这样做的好处是, 后面其他地方需要使用, 我这里也能直接调用这个函数, 常见的场景比如用户的创建和修改, 我们都需要对入参字段做这些校验, 修改逻辑可能还需要查询这个用户是否还存在, 避免被其他地方同步删除的场景.
  2. 校验逻辑组合, 我们拆分了校验的逻辑, 那么在最外层具体的业务中, 我们势必也要去做if判断, 只是从最先的小逻辑判断, 变成了字段属性判断, 粒度变粗了而已, 如果一个结构体字段很多, 但是每个字段又需要处理, 这样的话还是会显得很臃肿, 这些是为什么会在这基础上诞生执行链这个工具的原因.

if不会消失, 只会换一种方式陪在我们身边, 你说呢

重点 执行链这种设计, 不仅仅局限于用在字段校验上(字段校验也许用go比较火的校验工具更加优雅些), 它是为了降低我们程序的耦合程度, 逻辑可插拔而设计, 就像前面在做Chain定义的时候说到的, 我们可以给Chain增加额外的属性和方法, 使得它具备额外的功能, 扩展其他能力, 比如加入状态, 执行链里面的A情况我执行下标是偶数的, B情况之下下标是奇数的, 再比如, 我可以加入并发的思想,让整个链路并发执行, 或者局部并发执行, 这些都是可以在此基础上扩展得到的能力, 也许还有其他场景也能套用, 欢迎您加入讨论和提出建议