Design mode - responsibility chain mode ~ Sunset Glow

Chain of Responsibility Pattern

TitleModuleCategoryTags
Chain of Responsibility
chain-of-responsibility-design
Behavioral
Gang of Four

background

When faced with decoupling between the request sender and multiple request handlers, the responsibility chain can deal with this situation well, that is, all request handlers build a chain by marking the reference of the previous object to its next object. When a request is issued, the request can be passed along the chain until an object processes it

Introduction to responsibility chain model

The responsibility chain mode is mainly used to decouple the request and processing logic. The client only needs to send the request to the link without paying attention to the processing details and contents of the request. The request will be automatically transmitted until there is a node object for processing. Each processing node can be regarded as a scheduler and send instructions to each node to form a responsibility tree, In some cases, recursive calls occur

Structure diagram

The responsibility chain model mainly includes two roles:
  • Abstract Handler: the Handler abstracts the method of request processing and maintains the reference of the Handler object of the next processing node
  • Specific handler: ConcreteHandler handles the request and forwards it if it is not interested

The essence of the responsibility chain mode is to decouple the request and processing, so that the request can be transmitted and processed in the processing link. Its core is to combine the processing nodes into a chain structure, and allow the nodes themselves to decide whether to process or forward the request. The request flows, which is similar to a kind of streaming processing

Application scenario

  • Multiple objects can handle the same request, but which object should handle it is dynamically determined at run time
  • In a scenario where the receiver request is not explicitly specified, a request is submitted to one of multiple objects
  • A group of object processing requests can be dynamically specified, such as the verification logic of the permission verification framework. The permission processing of each dimension can be decoupled and then connected in series. Each need only be responsible for relevant responsibilities

Code example

According to the UML design diagram drawn above, an abstract processor Handler can be established first

package com.kyle.design.chain.general;

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

    protected Handler nextHandler;

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

    public abstract void handleRequest(String request);
}

Then, by inheriting the Handler, write concrete handlers ConcreteHandlerA and ConcreteHandlerB

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);
        }
    }
}

By establishing a client test class, simulate sending a request

package com.kyle.design.chain.general;

/**
 * @author : Kyle
 * @description : Client test class
 */
public class SendRequestDrive {

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

HandlerA passes the request to HandlerB for processing. The following is the test result

Open source application

  • Filter in JDK
/**
 * 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();
}


The Filter interface class in J2EE standard is equivalent to the abstract role of Handler in the responsibility chain pattern. How to realize the composition of responsibility chain? From another class, the last parameter of the doFilter() method, we can see that the type of another class is the FilterChain class

/**
 * 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;
}

Only one doFilter() method is defined in the FilterChain class. J2EE provides us with a specification. The specific processing logic needs to be implemented by the user, such as the design of the responsibility chain of the agent in Spring

  • The serialization Pipeline in Netty also adopts the responsibility chain mode

The bottom layer adopts the data structure of two-way linked list to connect the processors on the link. When the client's request arrives, netty thinks that all processors in Pipeline have the opportunity to process it. Therefore, all the stacked requests propagate from the beginning to the end node, and the message will not be released until the end node. Netty has a responsibility processing interface 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 has made a more fine-grained division of responsibility processing interface functions. Processors are mainly divided into two types, one is the stack processor ChannelInboundHandler, and the other is the stack processor channeloutbooundhandler. These two interfaces are inherited from ChannelHandler. However, finally, the processor nodes are added to the Pipeline, The responsibilities for adding and deleting processor nodes are also specified in 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();
}

In Netty's default implementation class, all handlers are concatenated into a linked list

/**
 * 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;
    }
}

For any node in the Pipeline, as long as it is not propagated down manually, this event will terminate the propagation at the current node. For the stacked data, it will be passed to the tail node for recycling by default. If the next propagation is not carried out, the event will terminate at the current node, and for the stacked data, writing the data back to the client also means the termination of the event

  • Design of authority verification responsibility chain in security framework Spring Security

Thinking summary

The key of the responsibility chain model lies in the division of the responsibilities of the processing nodes and the planning of the sequence of the processing nodes in the responsibility link. Compared with the decorator model, the difference is that for the decorator, all classes can process requests, while for the responsibility chain, there is exactly one class in the link to process requests

At the same time, the responsibility chain mode can be used in combination with the builder mode. Due to its natural chain structure characteristics, the builder mode can automatically chain assemble the processing node objects, so as to avoid the problems of complex chain structure assembly and different service responsibilities caused by only using the responsibility chain mode. In this way, the customer only needs to specify the processing node objects, Moreover, by introducing the builder mode, the construction of chain structure (specifying the order of processing nodes) can realize the definition of autonomy

advantage

  • Decouple the actual request from the receiver processing logic to facilitate the later extension of the new request processing class (processing node)
  • The processing link logic structure is flexible, and the responsibility processing logic can be dynamically added or deleted by changing the link structure
  • The request handler (node object) only needs to pay attention to the requests they are interested in, and the objects they are not interested in are directly forwarded to the next level node object
  • With the function of chain transmission and processing request, the request sender does not need to know the link structure, but only needs to wait for the request processing result

shortcoming

  • Too long responsibility chain or processing logic may affect the overall performance of system processing
  • If there is a circular reference to a node object, it will cause an endless loop and cause the system to crash

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

‚Äč

Posted on Fri, 03 Dec 2021 16:26:59 -0500 by BarmyArmy