16.责任链模式Chain Of Responsibility

1.初识职责链模式

使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。

  • Handler:定义职责的接口,通常在这里定义处理请求的方法,可以在这里实现后继链。
    ConcreteHandler:实现职责的类,在这个类里面,实现对在它职责范围内请求的处理,如果不处理,就继续转发请求给后继者。
    Client:职责链的客户端,向链上的具体处理者对象提交请求,让职责链负责处理。

2.体会职责链模式

2.1 场景问题——申请聚餐费

用来考虑这样一个功能:申请聚餐费用的管理。

申请聚餐费用的大致流程一般是:由申请人先填写申请单,然后交给领导审查,如果申请批准下来了,领导会通知申请人审批通过,然后申请人去财务核领费用,如果没有核准,领导会通知申请人审批未通过,此事也就此作罢了。

不同级别的领导,对于审批的额度是不一样的,比如:项目经理只能审批 500元以内的申请;部门经理能审批1000元以内的申请;而总经理可以审核任意额度的申请。

也就是说,当某人提出聚餐费用申请的请求后,该请求会由项目经理、部门经理、总经理之中的某一位领导来进行相应的处理,但是提出申请的人并不知道最终会由谁来处理他的请求,一般申请人是把自己的申请提交给项目经理,或许最后是由总经理来处理他的请求,但是申请人并不知道应该由总经理来处理他申请请求。

那么该怎样实现这样的功能呢?

2.2 不用模式的解决方案

有何问题:
上面的实现很简单,基本上没难度。仔细想想,这么实现有没有问题呢?
仔细分析申请聚餐费用的业务功能和目前的实现,主要面临着如下问题:

  • 1)聚餐费用申请的处理流程是可能会变动的。
  • 2)各个处理环节的业务处理也是会变动的。

如果采用上面的实现,要是处理的逻辑发生了变化,解决的方法,一个是生成一个子类,覆盖requestToProjectManager方法,然后在里面实现新的处理;另一个方法就是修改处理申请方法的源码。

总之都不是什么好方法,也就是说,如果出现聚餐费用申请的处理流程变化的情况,或者是出现各个处理环节的功能变化的时候,上面的实现方式是很难灵活的变化来适应新功能的要求的。

把上面的问题抽象一下:客户端发出一个请求,会有很多对象都可以来处理这个请求,而且不同对象的处理逻辑是不一样的。对于客户端而言,无所谓谁来处理,反正有对象处理就可以了。

而且在上述处理中,还希望处理流程是可以灵活变动的,而处理请求的对象需要能方便的修改或者是被替换掉,以适应新的业务功能的需要。

请问如何才能实现上述要求?

2.3 使用模式的解决方案

2.3.1 使用模式来解决的思路

仔细分析上面的场景,当客户端提出一个聚餐费用的申请,后续处理这个申请的对象,项目经理、部门经理和总经理,自然的形成了一个链,从项目经理.部门经理.总经理,客户端的申请请求就在这个链中传递,直到有领导处理为止。看起来,上面的功能要求很适合采用职责链来处理这个业务。

要让处理请求的流程可灵活的变动,一个基本的思路,那就是动态构建流程步骤,这样随时都可以重新组合出新的流程来。而要让处理请求的对象也要很灵活,那就要让它足够简单,最好是只实现单一的功能,或者是有限的功能,这样更有利于修改和复用。

职责链模式就很好的体现了上述的基本思路,首先职责链模式会定义一个所有处理请求的对象都要继承实现的抽象类,这样就有利于随时切换新的实现;其次每个处理请求对象只实现业务流程中的一步业务处理,这样使其变得简单;最后职责链模式会动态的来组合这些处理请求的对象,把它们按照流程动态组合起来,并要求它们依次调用,这样就动态的实现了流程。

这样一来,如果流程发生了变化,只要重新组合就好了;如果某个处理的业务功能发生了变化,一个方案是修改该处理对应的处理对象,另一个方案是直接提供一个新的实现,然后在组合流程的时候,用新的实现替换掉旧的实现就可以了。

2.3.2 使用模式来解决的类图

3.理解职责链模式

3.1 认识职责链模式

3.1.1 职责链模式的功能

职责链模式主要用来处理: “客户端发出一个请求,有多个对象都有机会来处理这一个请求,但是客户端不知道究竟谁会来处理他的请求 ”,这样的情况。也就是需要让请求者和接收者解耦,这样就可以动态的切换和组合接收者了。

要注意在标准的职责链模式里面,是只要有对象处理了请求,这个请求就到此为止,不再被传递和处理了。

如果是要变形使用职责链,就可以让这个请求继续传递,每个职责对象对这个请求进行一定的功能处理,从而形成一个处理请求的功能链。

3.1.2 隐式接收者

当客户端发出请求的时候,客户端并不知道谁会真正处理他的请求,客户端只知道他提交请求的第一个对象。从第一个处理对象开始,整个职责链里面的对象,要么自己处理请求,要么继续转发给下一个接收者。

也就是对于请求者而言,并不知道最终的接收者是谁,但是一般情况下,总是会有一个对象来处理的,因此称为隐式接收者。

3.1.3 如何构建链

职责链的链怎么构建呢?这是个大问题,实现的方式也是五花八门,归结起来大致有以下一些方式。

首先是按照实现的地方来说:

  • 1)可以实现在客户端,在提交请求前组合链,也就是在使用的时候动态组合链,称为外部链;
  • 2)可以在Handler里面实现链的组合,算是内部链的一种;
  • 3)可以在各个职责对象里面,由各个职责对象自行决定后续的处理对象,这种实现方式要求每个职责对象除了进行业务处理外,还必须了解整个业务流程。

按照构建链的数据来源,也就是决定了按照什么顺序来组合链的数据,又分为几种:

  • 1)一种就是在程序里面动态组合;
  • 2)可以通过外部,如数据库来获取组合的数据,这种属于数据库驱动的方式;
  • 3)还有一种方式就是通过配置文件传递进来,也可以是流程的配置文件。

如果是从外部获取数据来构建链,那么在程序运行的时候,会读取这些数据,然后根据数据的要求来获取相应的对象,并组合起来。

还有一种是不需要构建链,因为已有的对象已经自然构成链了,这种情况多出现在组合模式构建的对象树中,这样子对象可以很自然的向上找到自己的父对象。就像部门人员的组织结构一样,顶层是总经理,总经理下面是各个部门的经理,部门经理下面是项目经理,项目经理下面是各个普通员工,自然就可以形成:普通员工->项目经理->部门经理->总经理这样的链。

3.1.4 谁来处理

职责链中那么多处理对象,到底谁来处理请求呢,这个是在运行时期动态决定的。当请求被传递到某个处理对象的时候,这个对象会按照已经设定好的条件来判断,是否属于自己处理的范围,如果是就处理,如果不是就转发请求给下一个对象。

3.1.5 请求一定会被处理吗?

在职责链模式中,请求不一定会被处理,因为可能没有合适的处理者,请求在职责链里面从头传递到尾,每个处理对象都判断不属于自己处理,最后请求就没有对象来处理。这一点是需要注意的。

可以在职责链的末端始终加上一个不支持此功能处理的职责对象,这样如果传递到这里,就会出现提示,本职责链没有对象处理这个请求。

3.2 处理多种请求

前面的示例都是同一个职责链处理一种请求的情况,现在有这样的需求,还是费用申请的功能,这次是申请预支差旅费,假设还是同一流程,也就是组合同一个职责链,从项目经理.传递给部门经理.传递给总经理,虽然流程相同,但是每个处理类需要处理两种请求,它们的具体业务逻辑是不一样的,那么该如何实现呢?

1.简单的处理方式
要解决这个问题,也不是很困难,一个简单的方法就是为每种业务单独定义一个方法,然后客户端根据不同的需要调用不同的方法,还是通过代码来示例一下。注意这里故意的把两个方法做的有些不一样,一个是返回String类型的值,一个是返回boolean类型的值;另外一个是返回到客户端再输出信息,一个是直接在职责处理里面就输出信息。

2.通用请求的处理方式
上面的实现看起来很容易,但是仔细想想,这样实现有没有什么问题呢?

有一个很明显的问题,那就是只要增加一个业务,就需要修改职责的接口,这是很不灵活的,Java开发中很强调面向接口编程,因此接口应该相对保持稳定,接口一改,需要修改的地方就太多了,频繁修改接口绝对不是个好主意。

那有没有什么好方法来实现呢?分析一下现在变化的东西:

  • 1)一是不同的业务需要传递的业务数据不同;
  • 2)二是不同的业务请求的方法不同;
  • 3)三是不同的职责对象处理这些不同的业务请求的业务逻辑不同

现在有一种简单的方式,可以较好的解决这些问题。首先定义一套通用的调用框架,用一个通用的请求对象来封装请求传递的参数;然后定义一个通用的调用方法,这个方法不去区分具体业务,所有的业务都是这一个方法,那么具体的业务如何区分呢,就是在通用的请求对象里面会有一个业务的标记;到了职责对象里面,愿意处理就跟原来一样的处理方式,如果不愿意处理,就传递到下一个处理对象就好了。

接下来看看如何在不改动现有的框架的前提下,扩展新的业务,这样才能说明这种设计的灵活性。

3.3 功能链

在实际开发中,经常会出现一个把职责链稍稍变形的用法。在标准的职责链中,一个请求在职责链中传递,只要有一个对象处理了这个请求,就会停止。

现在稍稍变一下,改成一个请求在职责链中传递,每个职责对象负责处理请求的某一方面功能,处理完成后,不是停止,而是继续向下传递请求,当请求通过很多职责对象处理过后,功能也就处理完了,把这样的职责链称为功能链。

考虑这样一个功能,在实际应用开发中,在进行业务处理之前,通常需要进行权限检查、通用数据校验、数据逻辑校验等处理,然后才开始真正的业务逻辑实现。可以把这些功能分散到一个功能链中,这样做的目的是使程序结构更加灵活,而且复用性会更好,比如通用的权限检查就只需要做一份,然后就可以在多个功能链中使用了。

有些朋友看到这里,可能会想,这不是可以使用装饰模式来实现吗?没错,可以使用装饰模式来实现这样的功能,但是职责链会更灵活一些,因为装饰模式是在已有的功能上增加新的功能,多个装饰器之间会有一定的联系;而职责链模式的各个职责对象实现的功能,相互之间是没有关联的,是自己实现属于自己处理的那一份功能。

可能有些朋友会想到这很类似于在Web应用开发中的过滤器Filter,没错,过滤器链就类似于一个功能链,每个过滤器负责自己的处理,然后转交给下一个过滤器,直到把所有的过滤器都走完,然后进入到Servlet里面进行处理。最常见的过滤器功能,比如权限检查、字符集转换等,基本上都是Web应用的标配。

接下来在示例中,实现这样的功能:实现商品销售的业务处理,在真正进行销售的业务处理之前,需要对传入处理的数据,进行权限检查、通用数据检查和数据逻辑检查,只有这些检查都能通过的情况下,才说明传入的数据是正确的、有效的数据,才可以进行真正的业务功能处理。

3.4 职责链模式的优缺点

  • 请求者和接收者松散耦合
  • 动态组合职责
  • 产生很多细粒度对象
  • 不一定能被处理

4.思考职责链模式

4.1 职责链模式的本质

职责链模式的本质是:分离职责,动态组合

4.2 何时选用

  • 1)如果有多个对象可以处理同一个请求,但是具体由哪个对象来处理该请求,是运行时刻动态确定的。这种情况可以使用职责链模式,把处理请求的对象实现成为职责对象,然后把它们构成一个职责链,当请求在这个链中传递的时候,具体由哪个职责对象来处理,会在运行时动态判断
  • 2)如果你想在不明确指定接收者的情况下,向多个对象中的一个提交一个请求的话,可以使用职责链模式,职责链模式实现了请求者和接收者之间的解耦,请求者不需要知道究竟是哪一个接收者对象来处理了请求。
  • 3)如果想要动态指定处理一个请求的对象集合,可以使用职责链模式,职责链模式能动态的构建职责链,也就是动态的来决定到底哪些职责对象来参与到处理请求中来,相当于是动态指定了处理一个请求的职责对象集合

5.示例——论坛帖子的过滤

实例代码参考github
论坛用户发表帖子,但是常常会有用户一些不良的信息,如广告信息,涉黄信息,涉及政治的敏感词等。

定义所有责任链对象的父类:

/**
 * 帖子处理器
 */
public abstract class PostHandler {

    /**
     * 后继者
     */
    protected PostHandler successor;

    public void setSuccessor(PostHandler handler){
        this.successor = handler;
    }

    public abstract void handlerRequest(Post post);

    protected final void next(Post post){
        if(this.successor != null){
            this.successor.handlerRequest(post);
        }
    }
}

父类 Handler 主要封装了传递请求等方法,其中要注意的有:

  • successor,后继者,这个属性很重要,它保存了责任链中下一个处理器
  • 在 next() 方法中(方法名自己随便取),当请求传递到最后一个责任对象时,已经没有后继者继续处理请求了,因此要对 successor 做判空处理,避免抛出空指针异常。
  • 处理请求的handlerRequest 的入参和返回类型可以根据实际情况修改,可以在该方法中抛出异常来中断请求

广告处理器:

/**
 * 广告处理器
 */
public class AdHandler extends PostHandler {

    @Override
    public void handlerRequest(Post post) {
        //屏蔽广告内容
        String content = post.getContent();
        //.....
        content = content.replace("广告","**");
        post.setContent(content);

        System.out.println("过滤广告...");
        //传递给下一个处理器
        next(post);
    }
}

涉黄处理器:

/**
 * 涉黄处理器
 */
public class YellowHandler extends PostHandler {

    @Override
    public void handlerRequest(Post post) {
        //屏蔽涉黄内容
        String content = post.getContent();
        //.....
        content = content.replace("涉黄","**");
        post.setContent(content);

        System.out.println("过滤涉黄内容...");
        //传递给下一个处理器
        next(post);
    }
}

敏感处理器:

/**
 * 敏感词处理器
 */
public class SensitiveWordsHandler extends PostHandler {

    @Override
    public void handlerRequest(Post post) {
        //屏蔽敏感词
        String content = post.getContent();
        //.....
        content = content.replace("敏感词","**");
        post.setContent(content);

        System.out.println("过滤敏感词...");
        //传递给下一个处理器
        next(post);
    }
}

测试:

//创建责任对象
PostHandler adHandler = new AdHandler();
PostHandler yellowHandler = new YellowHandler();
PostHandler swHandler = new SensitiveWordsHandler();

//形成责任链
yellowHandler.setSuccessor(swHandler);
adHandler.setSuccessor(yellowHandler);

Post post = new Post();
post.setContent("我是正常内容,我是广告,我是涉黄,我是敏感词,我是正常内容");

System.out.println("过滤前的内容为:"+post.getContent());

adHandler.handlerRequest(post);

System.out.println("过滤后的内容为:"+post.getContent());

5.1 为什么用责任链模式?

直接将过滤不良信息写在一个方法里不行吗?比如:

public class PostUtil {

    public void filterContent(Post post){

        String content = post.getContent();

        content = content.replace("广告","**");
        content = content.replace("涉黄","**");
        content = content.replace("敏感词","**");

        post.setContent(content);
    }
}

如果后面要增加其他的功能,过滤其他类型的内容,我们还得修改上面的 filterContent 方法,违背了开闭原则。
因此我们需要使用责任链模式,能够在不修改已有代码的情况下扩展新功能。

6.案例

6.1 Servlet 中的 Filter

Servlet 中的过滤器 Filter 就是典型的责任链模式,假如我们要给每一次Http 请求都打印一个 log,就可以使用 Filter过滤器来实现:

public class LogFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

        System.out.println("write log");
        filterChain.doFilter(servletRequest,servletResponse);

    }

    @Override
    public void destroy() {

    }
}

然后将这个过滤器配置到 web.xml 中:

<filter>
  <filter-name>LogFilter</filter-name>
  <filter-class>com.zhoujun.filter.LogFilter</filter-class>
</filter>

<filter-mapping>
  <filter-name>LogFilter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

在上面LogFilter类中,我们可以看到servlet的责任链是通过 Filter 来实现的,这是一个接口,在doFilter中还用到了 FilterChain ,也是一个接口。通过查找源码,发现了 FilterChain 的其中一个实现类:

public class PassThroughFilterChain implements FilterChain {
    @Nullable
    private Filter filter;
    @Nullable
    private FilterChain nextFilterChain;
    @Nullable
    private Servlet servlet;

    public PassThroughFilterChain(Filter filter, FilterChain nextFilterChain) {
        Assert.notNull(filter, "Filter must not be null");
        Assert.notNull(nextFilterChain, "'FilterChain must not be null");
        this.filter = filter;
        this.nextFilterChain = nextFilterChain;
    }

    public PassThroughFilterChain(Servlet servlet) {
        Assert.notNull(servlet, "Servlet must not be null");
        this.servlet = servlet;
    }

    public void doFilter(ServletRequest request, ServletResponse response) throws ServletException, IOException {
        if (this.filter != null) {
            this.filter.doFilter(request, response, this.nextFilterChain);
        } else {
            Assert.state(this.servlet != null, "Neither a Filter not a Servlet set");
            this.servlet.service(request, response);
        }

    }
}

该类中的 Private FilterChain nextFilterChain; 相当于 PostHandler 中的后继者 Successor。

将我们自定义的 Filter 配置到 web.xml 中的操作就是将该对象添加到责任链上,Servlet 开发者帮我们完成了 setSuccessor() 的操作。

参考

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

推荐阅读更多精彩内容