设计模式——责任链模式

设计模式——责任链模式

一. 简介

责任链模式(Chain of Responsibility) 是行为型设计模式之一,其将链中每一个节点看作是一个对象,每个节点处理的请求均不同,且内部自动维护一个下一节点对象。当一个请求从链式的首端发出时,会沿着链的路径依次传递给每一个节点对象,直至有对象处理这个请求为止。

主要解决

责任链模式 解耦了请求与处理,客户只需将请求发送到链上即可,无需关心请求的具体内容和处理细节,请求会自动进行传递直至有节点对象进行处理。

二. 优缺点

优点

  • 解耦了请求与处理;
  • 请求处理者(节点对象)只需关注自己感兴趣的请求进行处理即可,对于不感兴趣的请求,直接转发给下一级节点对象;
  • 具备链式传递处理请求功能,请求发送者无需知晓链路结构,只需等待请求处理结果;
  • 链路结构灵活,可以通过改变链路结构动态地新增或删减责任;
  • 易于扩展新的请求处理类(节点),符合 开闭原则;

缺点

  • 责任链路过长时,可能对请求传递处理效率有影响;
  • 如果节点对象存在循环引用时,会造成死循环,导致系统崩溃;

三.使用场景

  • 多个对象可以处理同一请求,但具体由哪个对象处理则在运行时动态决定;
  • 在不明确指定接收者的情况下,向多个对象中的一个提交一个请求;
  • 可动态指定一组对象处理请求;

三.场景demo

首先来看下 责任链模式 的通用 UML 类图:


UML

从 UML 类图中,我们可以看到,责任链模式 主要包含两种角色:

  • 抽象处理者(Handler):定义一个请求处理的方法,并维护一个下一个处理节点 Handler 对象的引用;
  • 具体处理者(ConcreteHandler):对请求进行处理,如果不感兴趣,则进行转发;

以下是 责任链模式 的通用代码:

class Client {
    public static void main(String[] args) {
        Handler handlerA = new ConcreteHandlerA();
        Handler handlerB = new ConcreteHandlerB();
        handlerA.setnextHanlder(handlerB);
        handlerA.handleRequest("requestB");
    }

    static abstract class Handler {
        protected Handler mNextHandler;

        public void setnextHanlder(Handler successor) {
            this.mNextHandler = successor;
        }

        public abstract void handleRequest(String request);
    }

    static class ConcreteHandlerA extends Handler {

        @Override
        public void handleRequest(String request) {
            if ("requestA".equals(request)) {
                System.out.println(String.format("%s deal with request: %s", this.getClass().getSimpleName(), request));
                return;
            }
            if (this.mNextHandler != null) {
                this.mNextHandler.handleRequest(request);
            }
        }
    }
    static class ConcreteHandlerB extends Handler {

        @Override
        public void handleRequest(String request) {
            if ("requestB".equals(request)) {
                System.out.println(String.format("%s deal with request: %s", this.getClass().getSimpleName(), request));
                return;
            }
            if (this.mNextHandler != null) {
                this.mNextHandler.handleRequest(request);
            }
        }
    }
}

上面的代码中,其实我们把消息硬编码为String类型,而真实业务中,消息是具备多样性的,可以是int、String或者是自定义类型····因此,我们可以在上面代码中的基础上,将消息类型进行抽象Request,增强了消息的包容性。

责任链模式 的本质是:解耦请求与处理,让请求在处理链中能进行传递与被处理;理解 责任链模式 应当理解的是其模式(道)而不是其具体实现(术),责任链模式 的独到之处是其将节点处理者组合成了链式结构,并允许节点自身决定是否进行请求处理或转发,相当于让请求流动了起来。

个人认为:责任链模式就相当于一个链表的递归调用

三.OkHttp源码之拦截器链

对于okhttp来说,它的拦截器各位肯定是非常熟悉的,正是由于它的存在,okhttp的扩展性极强,对于调用方来说可谓是非常友好。而实现这个拦截器的就是大名鼎鼎的责任链模式了,其实整个okhttp的核心功能都是基于这个拦截器的,由各种不同的拦截器实现的。所以今天我们分析下okhttp的责任链模式。

基本源码

对于okhttp的Call来说,发起请求最终其实都调用了一个方法获取到Response的,同步异步都是一样的:

Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(retryAndFollowUpInterceptor);
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    interceptors.add(new CacheInterceptor(client.internalCache()));
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));

    Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
        originalRequest, this, eventListener, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    return chain.proceed(originalRequest);
  }

前面只是加入一些必要的拦截器,是一些具体功能的实现,我们今天并不是分析这些具体功能,而是分析这种特殊的架构,所以这些拦截器就略过,我们重点看这里

Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
        originalRequest, this, eventListener, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

return chain.proceed(originalRequest);

核心就是构造了一个chain,这个chain我们重点关注的是interceptors和index(此时是第一个头节点,传入的是0),也就是 RetryAndFollowUpInterceptor,然后调用chain.proceed()方法获取最终结果。
那么我们继续跟进去:

public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
      RealConnection connection) throws IOException {
     //此处省略其他代码

    // Call the next interceptor in the chain.
    RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
        connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
        writeTimeout);
    Interceptor interceptor = interceptors.get(index);
    Response response = interceptor.intercept(next);
     //此处省略其他代码
    return response;
  })

可以看到,每一个chain都是链条上的一个节点,chain的proceed()方法首先是获取下一个节点,然后获取当前节点对应的拦截器,将下一个节点当做参数给了拦截器的intercept()方法,在interceptor中会手动调用下一个chain的chain.proceed方法,将这条链走下去。这里有一点要注意,此处是先获取的下一个chain,也就是说我们在第n个拦截器中会调用第n+1个chain的proceed()方法,这样才能做到请求之前添加自定义行为(调用第n+1个chain的proceed()之前)和请求之后(调用第n+1个chain的proceed()之后)添加自定义行为。

疑问

现在,大家应该都知道这条链是怎么传下去的,但大家可能会感到很好奇,为什么这里会有两个角色,interceptor和chain,而且这两个结构似乎不是传统意义上的client和handler的角色(经典的责任链模式主要是这两个角色),因为这里interceptor中会调用chain的方法,这一行为似乎有点反常(此处的client角色应该是RealCall)。由于暴露给外界的接口其实是interceptor,这里我们尝试抛弃chain看会怎样,下面是一个链表结构的经典责任链实现

public class InterceptorChain {
    Response proceed(Request request){
        //此处意味着拿到第一个节点,具体怎么拿到不用管,仅仅做示意
        Interceptor first = getFirstInterceptor();
        return first.intercept(request);
    }

    public abstract class  Interceptor{
        protected Interceptor next;
        abstract Response intercept(Request request);
        void setNext(Interceptor interceptor){
            next = interceptor;
        }
    }
}

经典的责任链两个角色,client(此处的InterceptorChain)和handler(此处的Interceptor)
此处我们尝试去实现okhttp的日志拦截器,主要实现:

public class LogInterceptor extends Interceptor{

        @Override
        Response intercept(Request request) {
            //此处getRequestParams是伪代码,仅做示意
            Map<String,String> requestParams = request.getRequestParams();
            Response response= next.intercept(request);
            //此处getResult是伪代码,仅做示意
            String result = response.getResult();
            return response;
        }
}

可以看到,没有chain这个类,我们只是借助interceptor也是能实现现有的结构的。那么这两种结构有本质区别吗?我们再看下正常的okhttp的日志拦截器:

public class LogInterceptor2 implements okhttp3.Interceptor{

        @Override
        public Response intercept(Chain chain) throws IOException {
            //此处getRequestParams是伪代码,仅做示意
            Request request = chain.request();
            Map<String,String> requestParams = request.getRequestParams();
            Response response= chain.proceed(request);
            //此处getResult是伪代码,仅做示意
            String result = response.getResult();
            return response;
        }
}

可以看到代码几乎一模一样,只是一个直接调用interceptor实现,一个通过chain来实现的,然而区别就在于这个chain.proceed()方法中:

public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
      RealConnection connection) throws IOException {
    if (index >= interceptors.size()) throw new AssertionError();

    calls++;

    // If we already have a stream, confirm that the incoming request will use it.
    if (this.httpCodec != null && !this.connection.supportsUrl(request.url())) {
      throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
          + " must retain the same host and port");
    }

    // If we already have a stream, confirm that this is the only call to chain.proceed().
    if (this.httpCodec != null && calls > 1) {
      throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
          + " must call proceed() exactly once");
    }

    // Call the next interceptor in the chain.
    RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
        connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
        writeTimeout);
    Interceptor interceptor = interceptors.get(index);
    Response response = interceptor.intercept(next);

    // Confirm that the next interceptor made its required call to chain.proceed().
    if (httpCodec != null && index + 1 < interceptors.size() && next.calls != 1) {
      throw new IllegalStateException("network interceptor " + interceptor
          + " must call proceed() exactly once");
    }

    // Confirm that the intercepted response isn't null.
    if (response == null) {
      throw new NullPointerException("interceptor " + interceptor + " returned null");
    }

    if (response.body() == null) {
      throw new IllegalStateException(
          "interceptor " + interceptor + " returned a response with no body");
    }
    return response;
  }

总体看下来,本质区别其实就一个,okhttp的proceed()方法会被执行多次(在每个interceptor中都会调用chain.proceed()获取Response),每一个interceptor都会执行该方法,所以在该方法中我们可以对interceptor的实现做很多检查,做出一些约束。然而我们自己DIY的结构,我们最理想的也只是把这些检查封装成一种util,由interceptor的实现人员手动调用,然而靠interceptor开发人员调用的话很容易遗漏,所以这里应该是借用了代理的思想,将它封装在了chain中,必须通过chain的proceed()方法获取Response,这些就能由框架做这些检查和约束了。

总结

okhttp的这套责任链模式的实现,从代码上来看确实复杂了不少,理解起来没那么容易,但是,这套模式能够对每个interceptor的行为做出一些基本的规范和检查,而不是都扔给interceptor的实现人员去做,这点收益就大了,稍微难理解一点也是可以的,当然我们更应该学习的是这种将约束和检查收敛到框架中的这种想和实现思路,比如这里就打破了常规的责任链,利用了代理的思想引入了一个chain。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容