Golang 设计模式之责任链模式

文摘   科技   2023-07-28 18:13   北京  

1 责任链模式

本期和大家一起聊聊设计模式中的责任链模式.

有这么一类场景,我们需要按照指定顺序串行化地执行一系列的任务,前置的任务节点根据执行情况可以选择提前熔断流程或者正常向下调度.

在这类场景,我们适合使用设计模式中的责任链模式,将整个任务流搭建成一条由单向链表组成的责任链,帮助使用方隐藏掉链式执行的细节,让使用方应用起来更加方便快捷.

下面我们给出一个具体场景:我们有一个给予用户发放奖励的场景,但是在执行发奖前需要前置执行一系列校验逻辑,包括需要校验用户的兑奖令牌 token 是否合法,需要校验用户的年龄是否满足条件,需要校验用户是否已完成授权操作等一系列工作.

此时,倘若由你通过编程来设计实现这个场景,你会采取怎样的实现思路呢?

 

2 代码实现

针对第 1 章中抛出的场景问题,下面展示一下具体的实现代码示例.

2.1 遍历模式

首先我们容易想到的一种比较常规的实现思路是遍历模式.

我们将一系列需要执行校验工作排列成一个 list,每次在执行发奖操作前,我们都需要对这个 list 进行遍历,依次执行其中每一步的校验工作,如果其间某一步校验未能通过,则会前置抛出错误,从而终止整个发奖流程.

 

下面是具体的代码实现.

我们把每一步的检查工作定义成 RuleHandler 类型,本质上一个函数类型,通过是否返回错误来标识是否需要熔断发奖流程.

type RuleHandler func(ctx context.Context, params map[string]interface{}) error

 

针对于检查用户 token、检查用户年龄、检查用户授权状态三项检查工作,我们一一声明好对应的执行函数:

  • • 检查 token:

var checkTokenRule RuleHandler = func(ctx context.Context, params map[string]interface{}) error {
    // 校验 token 是否合法
    token, _ := params["token"].(string)
    if token != "myToken" {
        return fmt.Errorf("invalid token: %s", token)
    }


    return nil
}

 

  • • 检查年龄:

var checkAgeRule RuleHandler = func(ctx context.Context, params map[string]interface{}) error {
    // 校验 age 是否合法
    age, _ := params["age"].(int)
    if age < 18 {
        return fmt.Errorf("invalid age: %d", age)
    }


    return nil
}

 

  • • 检查授权状态:

var checkAuthorizedStatus RuleHandler = func(ctx context.Context, params map[string]interface{}) error {
    // 校验是否已授权
    if authorized, _ := params["authorized"].(bool); !authorized {
        return errors.New("not authorized yet")
    }


    return nil
}

 

下面针对这种遍历模式的使用示例. 我们提前组装好一个 ruleHandler list,按照 checkToken->checkAge->checkAuthor 的顺序依次执行校验逻辑,其间某一步校验未通过,则会抛出错误,终止发奖流程.

func Test_RuleChainV1(t *testing.T) {
    // 组装 ruleHandler 链
    ruleChain := []RuleHandler{
        checkTokenRule,
        checkAgeRule,
        checkAuthorizedStatus,
    }


    ctx := context.Background()
    params := map[string]interface{}{
        "token": "myToken",
        "age":   1,
    }
    
    // 按照顺序,遍历执行 ruleHandler
    for _, ruleHandler := range ruleChain {
        if err := ruleHandler(ctx, params); err != nil {
            // 校验未通过,终止发奖流程
            t.Error(err)
            return
        }
    }
     
    // 通过前置校验流程,进行奖励发放
    sendReward(ctx,params)
}

 

2.2 责任链模式

除了上述的遍历模式外,我们今天需要着重介绍的责任链模式同样能够很好地解决这个场景问题. 同时,我们可以对比看看,基于责任链模式相比于遍历模式,在哪些方面具有优势.

在责任链模式中,我们组装一条单向链表,用于表示整个校验执行流程,链表中的每个节点对应于一个执行步骤,彼此之前通过 next 指针进行串联,各节点位置关系反映出对应步骤在执行流程中的先后顺序.

责任链模式与遍历模式的区别在于,由每个节点自身决定如何去协调当前步骤的执行逻辑与后续节点执行逻辑之间的关系,并且在节点执行过程中,负责通过 next 指针完成对后继节点的串联执行.

这样的设计使得在使用方看来,调用一整条责任链看起来就像是在调用单个节点一般,各节点之间的拓扑结构和依赖关系都通过责任链内部的运行机制实现,外侧的使用方无需感知这部分细节.

 

 

下面,我们就基于责任链模式的设计思路来实现上述场景问题:

  • • 首先定义出一个抽象的责任链接口 RuleChain interface,接口中包含两个核心方法:

  • • Apply:执行当前节点的逻辑

  • • Next:返回当前节点的下一个节点

  • • 接下来定义一个基础的责任链实现类 baseRuleChain,其中封装了一些公共逻辑供继承类复用

  • • 最后定义三个继承了 baseRuleChain 的实现类 CheckTokenRule、CheckAgeRule 和 CheckAuthorizedStatus,分别在 Apply 方法中实现具体的检查逻辑,并保证在执行完本节点的检查逻辑后,会串联调用 next 节点

对应的类图结构展示如下图所示:

 

下面是具体的代码展示环节:

首先是对规则链 interface 进行定义:

type RuleChain interface {
    Apply(ctx context.Context, params map[string]interface{}) error
    Next() RuleChain
}

 

接下来定义一个实现了 RuleChain 的基础实现类 baseRuleChain,其中包含一个 next 指针,用于指向后继节点. 此外,还包含如下方法:

  • • Apply:baseRuleChain 的 Apply 方法未作具体实现,需要由继承类进行实现

  • • Next:baseRuleChain 的 Next 方法做了实现,直接返回内置的 next 节点. 所有继承类都可以复用这部分逻辑,无需重复实现

  • • applyNext:baseRuleChain 定义了一个公共方法 applyNext,用于判断 next 节点是否非空,非空则执行 next 节点. 各个继承类可以统一复用这个方法,进行相邻节点的串联

type baseRuleChain struct {
    next RuleChain
}


func (b *baseRuleChain) Apply(ctx context.Context, params map[string]interface{}) error {
    panic("not implement")
}


func (b *baseRuleChain) Next() RuleChain {
    return b.next
}


func (b *baseRuleChain) applyNext(ctx context.Context, params map[string]interface{}) error {
    if b.Next() != nil {
        return b.Next().Apply(ctx, params)
    }
    return nil
}

 

下面对三个具体的规则节点进行定义声明,包括 CheckTokenRule、CheckAgeRule、CheckAuthorizedStatus. 在这三个类对应的 Apply 方法中,分别执行了:

  • • 当前节点的校验逻辑

  • • 倘若当前节点的校验逻辑未通过,则抛出错误,终止流程

  • • 倘若当前节点校验通过,执行后继节点的校验逻辑

  • • 针对后继节点的响应结果可以进行一定的后处理工作

type CheckTokenRule struct {
    baseRuleChain
}


func NewCheckTokenRule(next RuleChain) RuleChain {
    return &CheckTokenRule{
        baseRuleChain: baseRuleChain{
            next: next,
        },
    }
}


func (c *CheckTokenRule) Apply(ctx context.Context, params map[string]interface{}) error {
    // 校验 token 是否合法
    token, _ := params["token"].(string)
    if token != "myToken" {
        return fmt.Errorf("invalid token: %s", token)
    }


    if err := c.applyNext(ctx, params); err != nil {
        // err post process
        fmt.Println("check token rule err post process...")
        return err
    }


    fmt.Println("check token rule common post process...")
    return nil
}

 

type CheckAgeRule struct {
    baseRuleChain
}


func NewCheckAgeRule(next RuleChain) RuleChain {
    return &CheckAgeRule{
        baseRuleChain: baseRuleChain{
            next: next,
        },
    }
}


func (c *CheckAgeRule) Apply(ctx context.Context, params map[string]interface{}) error {
    // 校验 age 是否合法
    age, _ := params["age"].(int)
    if age < 18 {
        return fmt.Errorf("invalid age: %d", age)
    }


    if err := c.applyNext(ctx, params); err != nil {
        // err post process
        fmt.Println("check age rule err post process...")
        return err
    }


    fmt.Println("check age rule common post process...")
    return nil
}

 

type CheckAuthorizedStatus struct {
    baseRuleChain
}


func NewCheckAuthorizedStatus(next RuleChain) RuleChain {
    return &CheckAuthorizedStatus{
        baseRuleChain: baseRuleChain{
            next: next,
        },
    }
}


func (c *CheckAuthorizedStatus) Apply(ctx context.Context, params map[string]interface{}) error {
    // 校验是否已认证
    if authorized, _ := params["authorized"].(bool); !authorized {
        return errors.New("not authorized yet")
    }


    if err := c.applyNext(ctx, params); err != nil {
        // err post process
        fmt.Println("check authorized status rule err post process...")
        return err
    }


    fmt.Println("check authorized statuse rule common post process...")
    return nil
}

 

对应上述责任链模式的使用示例如下所示:

func Test_RuleChainV2(t *testing.T) {
    checkAuthorizedRule := NewCheckAuthorizedStatus(nil)
    checkAgeRule := NewCheckAgeRule(checkAuthorizedRule)
    checkTokenRule := NewCheckTokenRule(checkAgeRule)


    if err := checkTokenRule.Apply(context.Background(), map[string]interface{}{
        "token": "myToken",
        "age":   1,
    }); err != nil {
        // 校验未通过,终止发奖流程
        t.Error(err)
        return 
    }
    
    // 通过前置校验流程,进行奖励发放
    sendReward(ctx,params)
}

 

最后,在基于责任链模式解决了具体的场景问题后,我们来对这种设计模式的优势进行总结:

  • • 为使用方屏蔽了链式调用串行执行的内部细节,使用方可以像是使用单个节点一样一键启动责任链

  • • 组装责任链时,由于后继节点实际上是由前置节点通过 next 指针进行调用的,因此前置节点可以获得后继节点的执行结果,并进行一轮后处理工作. 这个后处理的执行切面是遍历模式所不具有的

 

3 工程案例

实践出真知. 下面我们一起欣赏两个实际应用到责任链模式的工程实践案例.

3.1 grpc-go 框架中的 InterceptorChain

grpc-go开源地址:https://github.com/grpc/grpc-go

走读源码版本:v1.53.0

 

在 grpc-go 服务端中,基于责任链模式实现了拦截器链 interceptorChain 的执行.

每当有一笔 grpc 请求到达服务端后,服务端会根据请求的 path 匹配到对应的 handler,并且在执行 handler 之前,会先通过执行一个拦截器链 InterceptorChain,完成一些前后置附属逻辑的执行.

 

 

真正用于处理 grpc 请求的 handler 对应的类型是 UnaryHandler.

type UnaryHandler func(ctx context.Context, req interface{}) (interface{}, error)

 

拦截器的类型是 UnaryServerInterceptor,入参中包含了 handler,会在调用 handler 的基础上,额外执行一些附属逻辑.

type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)

 

在 chainUnaryInterceptors 方法中,实现了将拦截器链 Interceptor list 压缩成一个节点的方式. 这是一种另类的责任链的实现思路,但是代码逻辑有点复杂,初学者乍一看可能不太容易理解,不过没关系,下面我们就一起来解读一下.

在 chainUnaryInterceptors 方法中,通过闭包的方式,封装出一个对应于 UnaryServerInterceptor 类型的函数用于返回. 在这个函数中调用了 interceptor list 中的首个 interceptor,并且通过调用 getChainUnaryHandler,把后置的 interceptor 连带最终的处理函数 handler 封装成一个增强版的 ”handler“,作为首个 interceptor 入参中的“handler”.

func chainUnaryInterceptors(interceptors []UnaryServerInterceptor) UnaryServerInterceptor {
    return func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (interface{}, error) {
        return interceptors[0](ctx, req, info, getChainUnaryHandler(interceptors, 0, info, handler))
    }
}

 

在 getChainUnaryHandler 的执行逻辑中,会根据传入的索引值 index 进行判断,如果已经来到拦截器链 interceptor list 的末尾,则直接返回 handler 用于执行;否则通过闭包的方式,组装出一个 handler 类型的函数返回,在函数中执行的逻辑则是调用下一个位置的拦截器 interceptor,并且并且递归压栈调用 getChainUnaryHandler,将后置的 interceptor 连带最终 handler 生成的增强“handler”传给这个 interceptor.

func getChainUnaryHandler(interceptors []UnaryServerInterceptor, curr int, info *UnaryServerInfo, finalHandler UnaryHandler) UnaryHandler {
    if curr == len(interceptors)-1 {
        return finalHandler
    }
    return func(ctx context.Context, req interface{}) (interface{}, error) {
        return interceptors[curr+1](ctx, req, info, getChainUnaryHandler(interceptors, curr+1, info, finalHandler))
    }
}

 

针对 grpc-go 这种另类的责任链实现方式,我当初也是读了很多遍才略微得到理解. 下面我们一起着手来仿写一遍,做到加深巩固:

// 声明一个执行函数类
type Handler func(ctx context.Context, req []string) ([]string, error)


// 声明拦截器类
type Interceptor func(ctx context.Context, req []string, handler Handler) ([]string, error)


// 将拦截器压缩成链的方法
func chainInterceptors(interceptors []Interceptor) Interceptor {
    if len(interceptors) == 0 {
        return nil
    }
    // 返回一个拦截器 interceptor 类型的闭包执行函数
    return func(ctx context.Context, req []string, handler Handler) ([]string, error) {
        return interceptors[0](ctx, req, getChainHandler(interceptors, 0, handler))
    }
}


// 从 inteceptor list 的 index + 1 位置开始,结合 finnal handler,形成一个增强版的 handler
func getChainHandler(interceptors []Interceptor, index int, finalHandler Handler) Handler {
    if index == len(interceptors)-1 {
        return finalHandler
    }


    return func(ctx context.Context, req []string) ([]string, error) {
        return interceptors[index+1](ctx, req, getChainHandler(interceptors, index+1, finalHandler))
    }
}


// 声明好 finnal handler
var handler Handler = func(ctx context.Context, req []string) ([]string, error) {
    fmt.Printf("final handler is running, req: %+v", req)
    req = append(req, "finnal_handler")
    return req, nil
}


// 声明拦截器 1
var interceptor1 Interceptor = func(ctx context.Context, req []string, handler Handler) ([]string, error) {
    fmt.Println("interceptor1 preprocess...")
    req = append(req, "interceptor1_preprocess")
    resp, err := handler(ctx, req)
    fmt.Println("interceptor1 postprocess")
    resp = append(resp, "interceptor1_postprocess")
    return resp, err
}


// 声明拦截器 2
var interceptor2 Interceptor = func(ctx context.Context, req []string, handler Handler) ([]string, error) {
    fmt.Println("interceptor2 preprocess...")
    req = append(req, "interceptor2_preprocess")
    resp, err := handler(ctx, req)
    fmt.Println("interceptor2 postprocess")
    resp = append(resp, "interceptor2_postprocess")
    return resp,nil
}

 

接下来执行一下对应的单测代码:

func Test_interceptor_chain(t *testing.T) {
    chainedInterceptor := chainInterceptors([]Interceptor{
        interceptor1, interceptor2,
    })


    resp, err := chainedInterceptor(context.Background(), nil, handler)
    if err != nil {
        t.Error(err)
        return
    }


    t.Logf("resp: %+v", resp)
}

 

单测的输出结果为:

    /Users/didi/my_first_test/chain_test.go:67: resp: [interceptor1_preprocess interceptor2_preprocess finnal_handler interceptor2_postprocess interceptor1_postprocess]

可以看到,输出顺序为【拦截器1前处理】->【拦截器2前处理】->【finnal handler】-> 【拦截器2后处理】-> 【拦截器1后处理】,符合递归压栈调用的执行顺序.

 

3.2 gin 框架中的 HandlerChain

gin 开源地址:https://github.com/gin-gonic/gin

走读的源码版本为 v1.9.0

作为 Golang 世界中最流行的 web 框架,gin 框架的一大优势就是其简单易用的中间件功能.

gin 框架中间件函数链 handlerChain 的定位和 grpc-go 中的拦截器链 interceptorChain 非常类似,在实现时也应用到了责任链模式的设计思路.

首先,gin 框架针对每笔到来的 http 请求,会分配一个 gin.Context 实例,完成整个请求链路的串联,在 gin.Context 中有一个核心字段 handlers,关联了用于处理本次请求的函数链 handlerChain,而另一个字段 index 则标识出当前已经执行到 handlerChain 中索引为 index 的 handler 了.

type Context struct {
    // ...
    // http 请求参数
    Request   *http.Request
    // http 响应 writer
    Writer    ResponseWriter
    // ...
    // 处理函数链
    handlers HandlersChain
    // 当前处于处理函数链的索引
    index    int8
    // ...
}

 

gin 当中 handlerChain 的执行顺序如下图所示:

 

在处理请求的时候,会以 gin.Context.Next 方法作为入口(每次 gin.Context 被重置时,index 会被设置为 -1),从头开始依次执行 handlerChain 中的每个 handler.

func (c *Context) Next() {
    c.index++
    for c.index < int8(len(c.handlers)) {
        c.handlers[c.index](c)
        c.index++
    }
}

 

在每个 handler 的执行过程中,也可以在调用后继 handler 的基础上,通过前/后处理的切面,添加一些附属增强逻辑:

func myHandleFunc(c *gin.Context){
    // 前处理
    preHandle()  
    c.Next()
    // 后处理
    postHandle()
}

 

倘若在执行到某个 handler 时触发了拦截机制,则可以通过调用 gin.Context.Abort 方法,直接将 gin.Context.index 置为一个非法值,实现执行流程的熔断效果.

func myHandleFuncWithAbort(c *gin.Context){
    // 前处理
    if err := preHandle();err != nil{
         c.Abort()
    } else{
         c.Next()
    }
   
    // 后处理
    postHandle()
}

 

const abortIndex int8 = 63


func (c *Context) Abort() {
    c.index = abortIndex
}

 

下面我们也来仿照 gin 框架来实现一把,加深一下理解:

// 每个执行函数的类型
type Handler func(ctx *Context, req map[string]interface{}) (map[string]interface{}, error)


// 串联流程的 Context 
type Context struct {
    index    int
    handlers []Handler
}


// Context 构造器函数,需要按照执行顺序注入一系列执行 handler
func NewContext(handlers ...Handler) *Context {
    return &Context{
        index:    -1,
        handlers: handlers,
    }
}


// handler 链最大长度为 100
const MaxHandlersCnt = 1

 

遍历调度 Context 中的 handlerChain,一旦 Context.Index >= 100,则遍历流程会被终止.

func (c *Context) Next(req map[string]interface{}) {
    c.index++
    for c.index < len(c.handlers) && c.index <= MaxHandlersCnt {
        c.handlers[c.index](c, req)
        c.index++
    }
}

 

声明三个 handler,预期按照 middleware1-> middleware2 -> finalHandler 的顺序执行:

var finalHandler Handler = func(ctx *Context, req map[string]interface{}) (map[string]interface{}, error) {
    req["final_handler"] = true
    return req, nil
}


var middleware1 Handler = func(ctx *Context, req map[string]interface{}) (map[string]interface{}, error) {
    req["middleware1_preprocess"] = true
    ctx.Next(req)
    req["middleware1_postprocess"] = true
    return req, nil
}


var middleware2 Handler = func(ctx *Context, req map[string]interface{}) (map[string]interface{}, error) {
    req["middleware2_preprocess"] = true
    ctx.Next(req)
    req["middleware2_postprocess"] = true
    return req, nil
}

 

下面是使用示例:

func Test_interceptor_chain(t *testing.T) {
    ctx := NewContext(middleware1, middleware2, finalHandler)
    params := map[string]interface{}{}
    ctx.Next(params)
    t.Logf("params: %+v", params)
}

 

该单测代码对应的输出结果为:

    /Users/didi/my_first_test/chain_test.go:129: params: map[final_handler:true middleware1_postprocess:true middleware1_preprocess:true middleware2_postprocess:true middleware2_preprocess:true]

 

下面我们同样实现以下 Abort 提前熔断处理流程的功能:

Abort 方法的处理逻辑就是将 Context.index 置为 MaxHandlersCnt,从而保证 Context.Next 方法的遍历流程会被终止.

func (c *Context) Abort() {
    c.index = MaxHandlersCnt
}

 

下面是一个执行了 Abort 逻辑的 handler:

var middleware1WithAbort Handler = func(ctx *Context, req map[string]interface{}) (map[string]interface{}, error) {
    req["middleware1_preprocess"] = true
    ctx.Abort()
    return req, nil
}

 

接下来我们给出使用到 Abort 逻辑的单测示例代码:

func Test_interceptor_chain_with_abort(t *testing.T) {
    ctx := NewContext(middleware1WithAbort, middleware2, finalHandler)
    params := map[string]interface{}{}
    ctx.Next(params)
    t.Logf("params: %+v", params)
}

 

由于 middleware1 在执行完前处理逻辑后就熔断了流程,因此单测的输出结果为:

    /Users/didi/my_first_test/chain_test.go:137: params: map[middleware1_preprocess:true]

 

4 总结

本期和大家一起探讨了设计模式中的责任链模式. 责任链模式的适用于当我们需要按照指定顺序串行化执行系列任务的场景,责任链模式所带来的优势在于:

  • • 为使用方屏蔽了链式调用串行执行的内部细节,使用方可以像是使用单个节点一样一键启动责任链

  • • 组装责任链时,由于后继节点实际上是由前置节点通过 next 指针进行调用的,因此前置节点可以获得后继节点的执行结果,并进行一轮后处理工作



小徐先生的编程世界
在学钢琴,主业码农
 最新文章