业务描述
两个接口(新增Tag和更新Tag),在业务层均需要添加两个校验,校验Tag信息是否重复和Tag的数据中的编码是否重复。
基本实现
方式
对应的增加两个校验的函数/方法,在接口实现中依次调用两个函数/方法进行校验。
优缺点
实现简单;但重复代码多,后期再增加其他校验,扩展性较差。
高阶函数方式一
方式
因为业务方法参数相同,业务层新增一个方法,包含一个函数类型参数(该函数即最终业务函数,新增/更新Tag),业务层新增方法:
Go
func (t *TagUseCase) ValidateTag(ctx context.Context, req *Tag, f func(context.Context, *Tag) error) error {
//校验一
exists, err := t.repo.ValidateAppIdFieldExists(ctx, req.AppId, req.Field, req.IdString)
if err != nil {
return err
}
if exists {
return errors.New(fmt.Sprintf("已存在:%v", req.Field))
}
//校验二
flag, code := t.ValidateCodeUnique(req.Children)
if flag {
return errors.New(fmt.Sprintf("编码重复:%v", code))
}
//执行业务目标函数
return f(ctx, req)
}
服务层调用:
Go
func (s *TagService) CreateTag(ctx context.Context, req *pb.CreateTagRequest) (*pb.OperationTagReply, error) {
//s.tuc.CreateTag为目标函数
err := s.tuc.ValidateTag(ctx, tag, s.tuc.CreateTag)
if err != nil {
return nil, errors.New(0, err.Error(), "failed!")
}
return &pb.OperationTagReply{Msg: "success"}, nil
}
优缺点
通过将目标函数参数化,将校验抽取到了一个方法中,后期如果增加其他校验,只需修改ValidateTag方法即可,有点类似于Java中的静态代理。重复代码很少,扩展性较好。但如果不同的业务需要的校验不完全相同,则存在问题。
高阶函数方式二
方式
通过中间件实现(借鉴Kratos框架middleware),校验函数和目标函数的入参和返回值均相同(即使不同,可通过golang的闭包将函数包装成需要的函数签名),将其都作为一次处理,将所有的处理链接起来再执行。
定义中间件Handler类型:
Go
定义中间件
type Handler func(ctx context.Context, req interface{}) error #定义handler类型,即定义中间件和最终执行方法/函数的的声明
type Middleware func(Handler) Handler # 定义Middle类型,即入参和返回值为相同类型的Handler,用于后面将其链接起来
# 将各中间件链接起来,next即最终要执行的函数,通过反向遍历的方式,将中间件按照添加的顺序依次链接,最先添加的在最外层,最先执行,结构类似:func(func(func(next)))
func Chain(m ...Middleware) Middleware {
return func(next Handler) Handler {
for i := len(m) - 1; i >= 0; i-- {
next = m[i](next)
}
return next
}
}
在业务层创建中间件:
Go
func (t *TagUseCase) ValidatorTagExists() tagmiddleware.Middleware {
return func(handler tagmiddleware.Handler) tagmiddleware.Handler {
return func(ctx context.Context, req interface{}) (err error) {
if v, ok := req.(*Tag); ok {
exists, err := t.repo.ValidateAppIdFieldExists(ctx, v.AppId, v.Field, v.IdString)
if err != nil {
return err
}
if exists {
return errors.New(fmt.Sprintf("该标签field已存在:%v", v.Field))
}
}
return handler(ctx, req)
}
}
}
调用方式一:
Go
#添加中间件:在业务层的结构体增加一个middleware字段,创建结构体时,将需要的中间件添加到该属性中
t.middleware = []tagmiddleware.Middleware{t.ValidatorTagExists(), t.ValidatorTagCodeRepeat()}
#提供给需要中间件的方法/函数调用: Chain首先获取到执行链,最终传入h进行调用
func (t *TagUseCase) Middleware(h tagmiddleware.Handler) tagmiddleware.Handler {
return tagmiddleware.Chain(t.middleware...)(h)
}
Go
最终调用:首先构建中间件,UpdateTag是最终要执行的方法。middleware(ctx,tag) 执行中间件和目标方法
middleware := s.tuc.Middleware(func(ctx context.Context, disposeReq interface{}) error {
return s.tuc.UpdateTag(ctx, disposeReq.(*biz.Tag))
})
err = middleware(ctx, tag)
调用方式二:方式一提前指定了要执行那些中间件,不够灵活
Go
#最终调用:在调用时指定要执行那些中间件。
middlewarex := tagmiddleware.Chain([]tagmiddleware.Middleware{s.tuc.ValidatorTagExists(), s.tuc.ValidatorTagCodeRepeat()}...)(func(ctx context.Context, disposeReq interface{}) error {
return s.tuc.UpdateTag(ctx, disposeReq.(*biz.Tag))
})
err = middlewarex(ctx, tag)
优缺点
将校验和目标函数都作为handler处理,在调用时可以自定义设置要进行那些校验,灵活性高。但后期增加新的校验时,需要在多个调用的位置将新的校验handler添加到执行链中。