活学活用责任链 | 射鸡模式

3,437 阅读9分钟

前文

相信大家对Okhttp的责任链模式应该已经很熟悉了,今天给大家普及另外一种有意思的玩法。

先把场景给大家描述下,省的各位大佬说我过度设计代码。我就以我写的谷歌支付作为例子吧。谷歌支付对大部分调用场景都是采用async异步接口回调的方式进行封装的。

  1. 与谷歌pay建立链接,如果链接失败,进行三次重试之后还是失败则结束。
  2. 查询是不是又没有验证的订单,如果有则处理,如果没有则向下执行。
  3. 查询Sku数据,如果失败则结束,成功之后继续逻辑执行。
  4. 根据Sku数据进行支付,之后异步获取支付结果,然后根据返回值判断是否向下执行。
  5. 调用Api通知后端交易结果,如果失败进行重试,如果还是失败则终止,成功继续向下执行。
  6. 判断订单是不是有库存商品,如果是则调用核销Api,没有则继续向下。
  7. 判断订单是不是订阅,如果是订阅则需要调用确认api,根据异步结果最后结束整条支付流程。

这个时候你们要说了,这不就是恶心一点点而已吗,我上我也行啊。一般会有这么几种大佬处理方式。

  • 异步大佬,异步套异步套异步,一把梭哈就是干,了不起就是7层嵌套吗。

  • 同步大佬,道理上来说万物可同步化,我只要子线程while true去等待取值,就可以把所有的异步都转化成同步。

  • RxJava或者协程大佬,这不就是个简单的链式操作,每个链每个function只负责做他们相关的操作,就是异步转化成Rxjava可能恶心了一点点,但是问题并不大。

抛出一个问题,RxJava是如何实现顺序链式执行的? 有没有觉得和OkHttp的责任链有点相似呢? 马萨卡!

一个例子理解Rxjava的事件流转换原理 , 有兴趣的同学可以看下这篇文章的分析。

正文

我们这次的解决方案就是采用责任链的方式对这有执行顺序的代码进行一次改造。这里我首先要感谢下wmrouter的作者,某微软大佬,大佬的代码给了我很好的启发,也就是从wmrouter中我基本掌握了这种好玩的设计模式。

Demo的话大家可以参考下我以前写的路由项目,也是基于这种异步责任链开发的。

路由Demo项目链接

责任链介绍

责任链模式是一种设计模式。在责任链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。发出这个请求的客户端并不知道链上的哪一个对象最终处理这个请求,这使得系统可以在不影响客户端的情况下动态地重新组织和分配责任。(参考百度百科)

我偷了两张网图介绍okhttp的责任链的,相对来说还是比较形象的。

image.png

整体拦截器的流程如下图所示。

image.png

简单的说,责任链就是按照特定的顺序向下执行的一个过程。大家了解最多的应该是okhttp的责任链。我们先从这个讲起,比较两部分差异。

fun interface Interceptor {
  @Throws(IOException::class)
  fun intercept(chain: Chain): Response


  interface Chain {
    fun request(): Request

    @Throws(IOException::class)
    fun proceed(request: Request): Response
  }
}

OkHttp的责任链是由一个Interceptor和一个Chain的接口开始的。然后通过这个Chainproceed方法执行下一个拦截器内的代码。由于proceed方法因为有返回值,所有必须确保下一个拦截器中有返回值才行。这就是说proceed方法必须要是一个同步的方法(当场有返回值的方法)。

无返回值的责任链

这个时候你说了,那么既然要求了下一个责任链必然要有返回,那么还咋实现异步责任链啊?这部分还是用之前路由项目来介绍吧,毕竟谷歌支付是公司代码(我被开除了你们养我???), 不是特别方便开源。

interface Interceptor {
    @Throws(RouteNotFoundException::class)
    fun intercept(chain: Chain)

    interface Chain {

        val url: KRequest

        val context: Context

        @Throws(RouteNotFoundException::class)
        fun proceed()
    }
}

对比观察下,其实两个责任链最大的差别就是intercept方法有没有具体的返回值。而proceed则只负责告诉当前拦截器是否向下执行了。简单的说两种责任链模式最大的区别就在这个地方了。

class LogInterceptor : Interceptor {

    override fun intercept(chain: Interceptor.Chain) {
        Log.i("LogInterceptor", "LogInterceptor")
        chain.proceed()
    }
}

正常情况下,我们的拦截器逻辑会和上面的代码一样,但是由于其实当前方法是没有返回值的,也就是说我们的一个异步任务只要持有了chain的实例,那么当我们的异步有结果回调了之后,再执行我们的proceed方法继续执行下一个链式调用是不是就可以了。下面我们上一个异步的实现看看。

class DelayInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain) {
        GlobalScope.launch {
            // 建立谷歌链接
            delay(5000)
            withContext(Dispatchers.Main) {
                chain.proceed()
            }
        }
    }
}

我在这个拦截器中使用了协程的延迟,delay代表使用了一个前文中提建立谷歌链接,然后我们调度到了主线程中之后调用责任链的下一个。因为fun是没有返回值的,而我们持有了chain的引用,所以我们可以在任意的异步中调用责任链的下一个,这样就完成了一个可异步的责任链封装了。

实现Chain

以我的路由的责任链为例子,其实proceed方法是非常简单粗暴的,通过index+1的方式调用下下一个拦截器,然后执行他的代码就行了,如果list为空则返回。

class RealInterceptorChain internal constructor(private val interceptors: List<Interceptor>, override val url: KRequest,
                                                override val hostParams: Map<String, HostParams>?,
                                                private val index: Int, override
                                                val context: Context) : Interceptor.Chain {

    @Throws(RouteNotFoundException::class)
    override fun proceed() {
        proceed(url)
    }

    @Throws(RouteNotFoundException::class)
    fun proceed(request: KRequest) {
        if (index >= interceptors.size) {
            return
        }
        val next = RealInterceptorChain(interceptors, request, hostParams,
                index + 1, context)
        val interceptor = interceptors[index]
        interceptor.intercept(next)
    }

}

另外一个则是需要有一个地方专门去把整个链式调用的拦截器放在一个列表内,对外使用的则是这个Call类。

class RealCall(private val hostMap: Map<String, HostParams>, private val config: RouteConfiguration) {

    private val cachedRoutes: HashMap<String, RouterParams> = hashMapOf()

    @Throws(RouteNotFoundException::class)
    fun open(request: KRequest, context: Context) {
        getParamsWithInterceptorChain(request, context)
    }

    @Throws(RouteNotFoundException::class)
    private fun getParamsWithInterceptorChain(request: KRequest, context: Context) {
        val interceptors: MutableList<Interceptor> = ArrayList()
        if (config.lazyInitializer) {
            interceptors.add(LazyInitInterceptor())
        }
        interceptors.addAll(config.interceptors)
        interceptors.add(CacheInterceptor(cachedRoutes))
        interceptors.add(RouterInterceptor(cachedRoutes))
        val chain: Interceptor.Chain = RealInterceptorChain(interceptors, request, hostMap, 0, context)
        chain.proceed()
    }
}

RealCall通过这样一个List以及对外暴露的open方法,我们就能确保这个责任链的顺序向下执行了。而且由于上面介绍到的Chain是通过proceed方法来通知下一个链被执行的。所有整个链就会被串联起来了。

在谷歌Pay中,因为他们其实并不算是个拦截器,而是一个处理器,所以这部分被我定义成了Handler。每个Handler负责处理自己当前所需要的那部分逻辑,当他完成之后则交给下一个Handler继续执行。

这部分可以用伪代码给大家介绍下。

连接Handler{
  async{
      if(连接成功){
        下一个Handler
      }else{
        重连试试看 3次则都失败返回
      }  
  }
}

谷歌掉单Handler{
  async{
    if(没有掉单){
      下一个Handler
    }else{
      终止
    }
  }
}

其他handler
...

我写了两个伪代码,代表了这部分Handler的处理逻辑,其他的都按照类似的去处理就行了。这样写的好处就是每一个由于每一个Handler只负责自己的逻辑,这样后续在升级维护过程中就会相对来说比较简单,而且中间可以插入一些别的处理器,或者调整顺序方便代码复用等。

而且也可以避免出现回调地狱的情况,如果代码只有成功回调就算了,万一还有异常则就是一坨稀泥了。说句最难听的万一我滚蛋了,后面的人只要按照责任链顺序查看代码逻辑就可以简单的了解其实现了。

有可能出现的问题

那么这种写法还有什么不足之处吗?

因为全部都是异步操作,而异步则意味着要持有引用,所以没有和LifeCycle绑定的情况下容易出现内存泄露的状况。举个例子,页面销毁了然后回调了结果,之后因为持有了回调,就会导致内存泄露的情况发生了。

这个时候我们提供另外一个终止的指令可以帮助我们优化泄露的情况。当当前责任链终止,则清空所有的Handler引用。

interface Chain {

     val context: Context

     val lifecycleOwner: LifecycleOwner?

     fun proceed()

     fun onComplete()
 }
class RealCall:Handler.Chain,LifecycleObserver{
  override fun onComplete() {
    // 清空你的责任链列表
    handlers.clear()
  }
}

还有就是因为全部都是异步的,所以会相对增加代码理解的难度,但是我个人觉得整体上来说还是要优于异步嵌套的。

同时由于方法并没有返回值,对于参数传递其实就并不是特别友好了,也只能通过异步的形式将结果传输给到使用方。虽然避免了大量的回调嵌套,但是还是要给使用方一个回调给予最后的处理结果的。

如果应用于类似路由这种场景的话,其实灵活性我感觉是远远比有返回值的拦截器要好很多的。

总结

如果这部分代码能让我使用协程去写,我觉得我完全信手拈来,没有任何压力。但是因为是一个sdk方,所以在输出代码的情况下要考虑到使用方有没有类库,而且最小依赖粒度的愿意,所以只能采取这种设计模式去让代码的依赖降到更少,而且让代码后面的可维护性更高一点。

这部分代码我也不是一开始就想到的,我也是先手写撸了一遍,然后想了想,如果过一阵子让我重新来接这部分代码,我估计会想死。所以就重新设计了下这部分代码的实现,多思考活学活用,不需要生搬硬套。