写在前面
我们在Go语言中, 经常会用到使用if的场景, 比如网络上关于go if的表情包
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
}
工具我们封装好了, 那上述的业务逻辑如何改进呢? 且看如下所示
- 首先拆分每个校验逻辑, 争取每个都是尽可能小的执行块
- 如果不能拆分的, 我们也可以把这一块看作是整体, 然后再把这个整体执行链加入更大的执行链
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, 被你这么一改, 代码还变多了, 我干嘛要这么写呢? 我需求还做不完呢!!!
为什么要这么做?
先来回顾一下上述, 我们总共做了两件事
- 拆分校验的业务逻辑, 把校验变成最小单元(其实这里还可以细分到具体的规则里面, 由于是举例说明, 就不再细分了), 这样做的好处是, 后面其他地方需要使用, 我这里也能直接调用这个函数, 常见的场景比如用户的创建和修改, 我们都需要对入参字段做这些校验, 修改逻辑可能还需要查询这个用户是否还存在, 避免被其他地方同步删除的场景.
- 校验逻辑组合, 我们拆分了校验的逻辑, 那么在最外层具体的业务中, 我们势必也要去做if判断, 只是从最先的小逻辑判断, 变成了字段属性判断, 粒度变粗了而已, 如果一个结构体字段很多, 但是每个字段又需要处理, 这样的话还是会显得很臃肿, 这些是为什么会在这基础上诞生执行链这个工具的原因.
if不会消失, 只会换一种方式陪在我们身边, 你说呢
重点 执行链这种设计, 不仅仅局限于用在字段校验上(字段校验也许用go比较火的校验工具更加优雅些), 它是为了降低我们程序的耦合程度, 逻辑可插拔而设计, 就像前面在做Chain定义的时候说到的, 我们可以给Chain增加额外的属性和方法, 使得它具备额外的功能, 扩展其他能力, 比如加入状态, 执行链里面的A情况我执行下标是偶数的, B情况之下下标是奇数的, 再比如, 我可以加入并发的思想,让整个链路并发执行, 或者局部并发执行, 这些都是可以在此基础上扩展得到的能力, 也许还有其他场景也能套用, 欢迎您加入讨论和提出建议