设计模式-责任链模式~晚霞

Chain of Responsibility Pattern

Title Module Category Tags
Chain of Responsibility chain-of-responsibility-design Behavioral
Gang of Four

背景

当面临将请求发送者与多个请求处理者之间进行解耦处理时, 责任链可以很好地应对这种情况, 即将所有请求的处理者通过前一对象标记其下一个对象的引用而构建成一条链, 当有请求发出时, 可将请求沿着这条链传递, 直到有对象处理它为止.

责任链模式简介

责任链模式主要是用于解耦请求和处理逻辑, 客户端只需要将请求发送到链路上即可, 而无需关心请求的处理细节及内容, 请求会自动进行传递直至有节点对象进行处理. 可以将各个处理节点看作一个个调度程序, 向各个节点发送指令, 形成一颗责任树, 某些情况下会出现递归调用情形.

结构图

设计模式-责任链模式~晚霞
责任链模式中主要包含两种角色:
  • 抽象处理者: Handler 抽象请求处理的方法, 并维护一个下一个处理节点 Handler 对象的引用
  • 具体处理者: ConcreteHandler 对请求进行处理, 若不感兴趣, 则进行转发

责任链模式的本质是解耦请求与处理, 让请求在处理链路中能进行传递与处理, 其核心在于将处理节点组合成链式结构, 并允许节点自身决定是否进行请求处理或转发, 请求流动起来, 类似一种流式处理.

应用场景

  • 多个对象可以处理同一请求, 但具体该由哪个对象处理则在运行时动态决定
  • 在不明确指定接收者请求的场景下, 向多个对象中的某个提交一个请求
  • 可动态指定一组对象处理请求, 如权限校验框架的校验逻辑, 将各个维度的权限处理解耦之后再串联起来, 各自只需负责相关的职责.

代码示例

可以根据上述绘制的 UML 设计图, 先建立抽象处理器 Handler

package com.kyle.design.chain.general;

/**
 * @author : Kyle
 * @description : 抽象 Handler
 */
public abstract class Handler {

    protected Handler nextHandler;

    public void setNextHandler(Handler successor) {
        this.nextHandler = successor;
    }

    public abstract void handleRequest(String request);
}

然后通过继承 Handler , 编写具体的处理者 ConcreteHandlerAConcreteHandlerB

package com.kyle.design.chain.general;

/**
 * @author : Kyle
 * @description :    ConcreteHandlerA
 */
public class ConcreteHandlerA extends Handler {

    public void handleRequest(String request) {
        if ("requestA".equals(request)) {
            System.out.println(this.getClass().getSimpleName() 
            					+ "deal with request: " + request);
            return;
        }
        if (this.nextHandler != null) {
            this.nextHandler.handleRequest(request);
        }
    }
}
package com.kyle.design.chain.general;

/**
 * @author : Kyle
 * @description :    ConcreteHandlerB
 */
public class ConcreteHandlerB extends Handler {

    public void handleRequest(String request) {
        if ("requestB".equals(request)) {
            System.out.println(this.getClass().getSimpleName() 
            					+ "deal with request: " + request);
            return;
        }
        if (this.nextHandler != null) {
            this.nextHandler.handleRequest(request);
        }
    }
}

通过建立客户端测试类, 模拟发出请求

package com.kyle.design.chain.general;

/**
 * @author : Kyle
 * @description : 客户端测试类
 */
public class SendRequestDrive {

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

HandlerA 通过传递请求至 HandlerB 进行处理, 以下是测试结果

设计模式-责任链模式~晚霞

开源应用

  • JDK 中的 Filter
/**
 * A filter is an object that performs filtering tasks on either the request to a resource (a servlet or static content), or on the response from a resource, or both.
 * <br>
 * Filters perform filtering in the <code>doFilter</code> method. Every Filter has access to
 * a FilterConfig object from which it can obtain its initialization parameters, a
 * reference to the ServletContext which it can use, for example, to load resources
 * needed for filtering tasks.

 */
public interface Filter {
    /**
     * Called by the web container to indicate to a filter that it is being placed into
     * service. The servlet container calls the init method exactly once after instantiating the
     * filter. The init method must complete successfully before the filter is asked to do any
     * filtering work. <br><br>
     * The web container cannot place the filter into service if the init method either<br>
     * 1.Throws a ServletException <br>
     * 2.Does not return within a time period defined by the web container
     */
    public void init(FilterConfig filterConfig) throws ServletException;

    /**
     * The <code>doFilter</code> method of the Filter is called by the container
     * each time a request/response pair is passed through the chain due
     * to a client request for a resource at the end of the chain. The FilterChain passed in to this
     * method allows the Filter to pass on the request and response to the next entity in the
     * chain.<p>
     * A typical implementation of this method would follow the following pattern:- <br>
     * 1. Examine the request<br>
     * 2. Optionally wrap the request object with a custom implementation to
     * filter content or headers for input filtering <br>
     * 3. Optionally wrap the response object with a custom implementation to
     * filter content or headers for output filtering <br>
     * 4. a) <strong>Either</strong> invoke the next entity in the chain using the FilterChain object (<code>chain.doFilter()</code>), <br>
     * 4. b) <strong>or</strong> not pass on the request/response pair to the next entity in the filter chain to block the request processing<br>
     * 5. Directly set headers on the response after invocation of the next entity in ther filter chain.
     */
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
    throws IOException, ServletException;

    /**
     * Called by the web container to indicate to a filter that it is being taken out of service. This
     * method is only called once all threads within the filter's doFilter method have exited or after
     * a timeout period has passed. After the web container calls this method, it will not call the
     * doFilter method again on this instance of the filter. <br><br>
     *
     * This method gives the filter an opportunity to clean up any resources that are being held (for
     * example, memory, file handles, threads) and make sure that any persistent state is synchronized
     * with the filter's current state in memory.
     */
    public void destroy();
}


J2EE 标准中的 Filter 接口类, 相当于责任链模式中的 Handler 抽象角色, 那么是如何实现责任链的构成的呢? 通过另一个类, doFilter() 方法的最后一个参数可以看出另一个类的类型就是 FilterChain

/**
 * A FilterChain is an object provided by the servlet container to the developer
 * giving a view into the invocation chain of a filtered request for a resource. Filters
 * use the FilterChain to invoke the next filter in the chain, or if the calling filter
 * is the last filter in the chain, to invoke the rosource at the end of the chain.
 *
 * @see Filter
 * @since Servlet 2.3
 */
public interface FilterChain {
    /**
     * Causes the next filter in the chain to be invoked, or if the calling filter is the last filter
     * in the chain, causes the resource at the end of the chain to be invoked.
     *
     * @param request the request to pass along the chain.
     * @param response the response to pass along the chain.
     *
     * @since Servlet 2.3
     */
    public void doFilter(ServletRequest request, ServletResponse response)
        throws IOException, ServletException;
}

FilterChain 类中只定义了一个 doFilter() 方法, J2EE 为我们提供了一种规范, 具体处理逻辑需要 使用者自己实现, 如 Spring 中对代理的责任链的设计

  • Netty 中的串行化 Pipeline 的处理也采用了责任链模式

底层采用双向链表的数据结构, 将链路上的各个处理器串联起来, 当客户端的请求到来时, Netty 认为 Pipeline 中所有的处理器都有机会处理它. 故而入栈的请求全部从头节点往后开始传播, 一直传播到尾节点才会把消息释放掉, Netty 中有一个责任处理接口ChannelHandler

public interface ChannelHandler {

    /**
     * Gets called after the {@link ChannelHandler} was added to the actual context
     * and it's ready to handle events.
     */
    void handlerAdded(ChannelHandlerContext ctx) throws Exception;

    /**
     * Gets called after the {@link ChannelHandler} was removed from the actual context 
     * and it doesn't handle events
     * anymore.
     */
    void handlerRemoved(ChannelHandlerContext ctx) throws Exception;

    /**
     * Gets called if a {@link Throwable} was thrown.
     *
     * @deprecated if you want to handle this event you should implement {@link ChannelInboundHandler} and
     * implement the method there.
     */
    @Deprecated
    void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception;

    /**
     * Indicates that the same instance of the annotated {@link ChannelHandler}
     * can be added to one or more {@link ChannelPipeline}s multiple times
     * without a race condition.
     * <p>
     * If this annotation is not specified, you have to create a new handler
     * instance every time you add it to a pipeline because it has unshared
     * state such as member variables.
     * <p>
     * This annotation is provided for documentation purpose, just like
     * <a href="http://www.javaconcurrencyinpractice.com/annotations/doc/">the JCIP annotations</a>.
     */
    @Inherited
    @Documented
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @interface Sharable {
        // no value
    }
}

Netty 对责任处理接口功能做了更加细粒度的划分, 处理器主要被分为两种, 一种是入栈处理器 ChannelInboundHandler, 另一种是出栈处理器 ChannelOutboundHandler, 这两个接口均是继承自 ChannelHandler. 不过最终处理器节点都添加在 Pipeline 上, 对于处理器节点的增删的职责也规定在 ChannelPipeline

public interface ChannelPipeline
        extends ChannelInboundInvoker, ChannelOutboundInvoker, Iterable<Entry<String, ChannelHandler>> {

    /**
     * Inserts a {@link ChannelHandler} at the first position of this pipeline.
     *
     * @param name     the name of the handler to insert first
     * @param handler  the handler to insert first
     */
    ChannelPipeline addFirst(String name, ChannelHandler handler);

    /**
     * Inserts a {@link ChannelHandler} at the first position of this pipeline.
     */
    ChannelPipeline addFirst(EventExecutorGroup group, String name, ChannelHandler handler);

    /**
     * Appends a {@link ChannelHandler} at the last position of this pipeline.
     */
    ChannelPipeline addLast(String name, ChannelHandler handler);

    /**
     * Inserts a {@link ChannelHandler} before an existing handler of this
     * pipeline.
     */
    ChannelPipeline addBefore(String baseName, String name, ChannelHandler handler);

    /**
     * Inserts a {@link ChannelHandler} after an existing handler of this
     * pipeline.
     */
    ChannelPipeline addAfter(String baseName, String name, ChannelHandler handler);

    /**
     * Inserts {@link ChannelHandler}s at the first position of this pipeline.
     */
    ChannelPipeline addFirst(ChannelHandler... handlers);

    /**
     * Inserts {@link ChannelHandler}s at the last position of this pipeline.
     */
    ChannelPipeline addLast(ChannelHandler... handlers);

    /**
     * Removes the first {@link ChannelHandler} in this pipeline.
     */
    ChannelHandler removeFirst();

    /**
     * Removes the last {@link ChannelHandler} in this pipeline.
     */
    ChannelHandler removeLast();
}

在 Netty 默认的实现类中是将所有的 Handler 串成一个链表

/**
 * The default {@link ChannelPipeline} implementation.  It is usually created
 * by a {@link Channel} implementation when the {@link Channel} is created.
 */
public class DefaultChannelPipeline implements ChannelPipeline {

    static final InternalLogger logger = InternalLoggerFactory.getInstance(DefaultChannelPipeline.class);

    private static final String HEAD_NAME = generateName0(HeadContext.class);
    private static final String TAIL_NAME = generateName0(TailContext.class);

    private static final AtomicReferenceFieldUpdater<DefaultChannelPipeline, MessageSizeEstimator.Handle> ESTIMATOR =
            AtomicReferenceFieldUpdater.newUpdater(
                    DefaultChannelPipeline.class, MessageSizeEstimator.Handle.class, "estimatorHandle");
    final AbstractChannelHandlerContext head;
    final AbstractChannelHandlerContext tail;

    private final Channel channel;
    private final ChannelFuture succeededFuture;
    private final VoidChannelPromise voidPromise;
    private final boolean touch = ResourceLeakDetector.isEnabled();

    private Map<EventExecutorGroup, EventExecutor> childExecutors;
    private volatile MessageSizeEstimator.Handle estimatorHandle;
    private boolean firstRegistration = true;

    /**
     * This is the head of a linked list that is processed by {@link #callHandlerAddedForAllHandlers()} and so process
     * all the pending {@link #callHandlerAdded0(AbstractChannelHandlerContext)}.
     *
     * We only keep the head because it is expected that the list is used infrequently and its size is small.
     * Thus full iterations to do insertions is assumed to be a good compromised to saving memory and tail management
     * complexity.
     */
    private PendingHandlerCallback pendingHandlerCallbackHead;


    private boolean registered;

    protected DefaultChannelPipeline(Channel channel) {
        this.channel = ObjectUtil.checkNotNull(channel, "channel");
        succeededFuture = new SucceededChannelFuture(channel, null);
        voidPromise =  new VoidChannelPromise(channel, true);

        tail = new TailContext(this);
        head = new HeadContext(this);

        head.next = tail;
        tail.prev = head;
    }
}

对于 Pipeline 中的任意一个节点, 只要不是人为手动的往下传播下去, 这个事件将会将传播终止在当前节点. 对于入栈数据, 默认会传递到尾节点进行回收. 若不进行下一步传播, 事件将会终止在当前节点, 而对于出栈数据把数据写回客户端也意味着事件的终止.

  • 安全框架 Spring Security 中权限校验责任链的设计

思考总结

​ 责任链模式关键在于处理节点职责的划分和处理节点在责任链路中顺序的规划. 与装饰器模式相比较而言, 存在的不同之处在于: 对于装饰器, 所有的类都能够处理请求, 而对于责任链, 链路中恰好有一个类处理请求.

​ 同时, 责任链模式可以与建造者模式结合使用, 由于其天然的链式结构特性, 通过建造者模式可以对处理节点对象进行自动地链式组装, 避免仅仅使用责任链模式引发的链式结构组装繁杂, 服务职责不单一的问题, 这样一来客户只需要指定处理节点对象, 而且引入建造者模式, 链式结构的构造(指定处理节点的顺序)可实现自主化的定义.

优点

  • 将实际请求与接受方处理逻辑解耦, 便于后期扩展新的请求处理类(处理节点)
  • 处理链路逻辑结构灵活, 可以通过改变链路结构动态新增或删减责任处理逻辑
  • 请求处理者(节点对象)仅需关注自身感兴趣的请求进行处理, 不感兴趣对象直接转发给下一级节点对象
  • 具备链式传递处理请求功能, 请求发送者无需知晓链路结构, 只需等待请求处理结果

缺点

  • 责任链过长或是处理逻辑过长, 可能会影响系统处理的整体的性能
  • 若是节点对象存在循环引用的时候, 会造成死循环, 导致系统可能崩溃

Reference

  • E.Gamma, R.Helm, R.Johnson, and Vlissides. Design Patterns Elements of Reusable Object Oriented Software. Addison-Wesley, 1995

  • Chain of Responsibility on Wiki

上一篇:操作数组的几个常用方法


下一篇:2021SC@SDUSC山东大学软件学院软件工程应用与实践--Ebiten代码分析 源码分析(九)