Architects must fully master the responsibility chain mode of the thorough way of kicking the ball

In our daily life, the responsibility chain mode is quite common. We usually work to handle some affairs, often all departments cooperate to complete a project task. Each department has its own responsibilities, so many times when a part of the work has been completed, it will be handed over to the next department, until all the departments have completed all the work, then the project task will be finally completed. There is also the usual saying that cutting off six generals after five passes is actually a mode of responsibility chain.

1, Application scenario of responsibility chain mode

The Chain of Responsibility Pattern regards each node in the chain as an object, each node handles different requests, and the next node object is automatically maintained internally. When a request is sent from the first end of the chain, it will be passed to each node object in turn along the path of the chain until an object processes the request. The responsibility mode is mainly to decouple the request and processing. As long as the customer sends the request to the corresponding chain, they don't need to care about the specific content and processing details of the request, and the request will be automatically delivered until the objects with nodes are processed.

The responsibility chain mode is applicable to the following scenarios:

  • Multiple objects can handle the same request, but the specific object processing is dynamically determined at runtime;
  • Submit a request to one of multiple objects without explicitly specifying the receiver;
  • You can dynamically specify a set of objects to process requests.

The responsibility chain model mainly includes two roles:

  • Abstract Handler: defines a method for request processing and maintains a reference to the Handler object of the next processing node;
  • Concrete handler: process the request and forward it if not interested.

The essence of the responsibility chain pattern is to decouple the request and processing, so that the request can be transferred and processed in the processing chain. To understand the responsibility chain pattern, we should understand its pattern (TAO) rather than its specific implementation. Its unique feature is that it forms a chain structure of node processors, and allows nodes themselves to decide whether to process or forward the request, which is equivalent to making the request flow Move up.

1.1 use responsibility chain mode for data verification and interception

First, create an entity class User object:

public class User {

    private String username;
    private String password;
    private String roleName;

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getRoleName() {
        return roleName;
    }

    public void setRoleName(String roleName) {
        this.roleName = roleName;
    }

    @Override
    public String toString() {
        return "User{" +
                "username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", roleName='" + roleName + '\'' +
                '}';
    }
}

Write a simple user login permission UserService class:

public class UserService {

    private final String ROLE_NAME = "administrator";

    public void login(String username, String password) {
        if(StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
            System.out.println("The user and password verification are successful. You can continue!");
            return;
        }
        System.out.println("The user and password cannot be empty. You can continue!");

        User user = check(username, password);
        if(null == user) {
            System.out.println("User does not exist!");
            return;
        }

        System.out.println("Congratulations, login succeeded!");

        if(!ROLE_NAME.equals(user.getRoleName())) {
            System.out.println("It is not an administrator, so there is no operation permission!");
        }

        System.out.println("Operation allowed!");
    }

    public User check(String username, String password) {
        User user = new User(username, password);
        user.setRoleName(ROLE_NAME);
        return user;
    }

    public static void main(String[] args) {
        UserService userService =  new UserService();
        userService.login("kevin", "123456");
    }
}

The main function of the above code is to verify the data before login, and the judgment logic is sequential. First, judge the non empty, then judge the user name and password, and finally get the user role according to the user name and password. If you have a role, you can get the user's operation permission. The implementation of such code in the business seems very bloated. We can use the responsibility chain mode to connect these inspection steps, so that we can pay more attention to the implementation of a specific business logic when coding.

First, create an abstract Handler class:

public abstract class Handler {

    protected Handler chain;

    public void next(Handler handler) {
        this.chain = handler;
    }

    public abstract void doHandle(User user);

}

Then create the ValidateHandler class, log in to verify LoginHandler class, and the AuthHandler class

ValidateHandler class:

public class ValidateHandler extends Handler {

    @Override
    public void doHandle(User user) {
        if(StringUtils.isEmpty(user.getUsername()) ||
                StringUtils.isEmpty(user.getPassword())) {
            System.out.println("The user and password verification are successful. You can continue!");
            return;
        }
        System.out.println("The user and password cannot be empty. You can continue!");
        chain.doHandle(user);
    }
}

LoginHandler class:

public class LoginHandler extends Handler {
    @Override
    public void doHandle(User user) {
        System.out.println("Congratulations, login succeeded!");
        user.setRoleName(ROLE_NAME);
        chain.doHandle(user);
    }
}

AuthHandler class:

public class AuthHandler extends Handler {
    @Override
    public void doHandle(User user) {
        if(!ROLE_NAME.equals(user.getRoleName())) {
            System.out.println("It is not an administrator, so there is no operation permission!");
        }

        System.out.println("Operation allowed!");
    }
}

Next, modify the UserService class to concatenate the handlers defined earlier:

public class UserService {

    public void login(String username, String password) {
        Handler validateHandler = new ValidateHandler();
        Handler loginHandler = new LoginHandler();
        Handler authHandler = new AuthHandler();

        validateHandler.next(loginHandler);
        loginHandler.next(authHandler);
        validateHandler.doHandle(new User(username, password));
    }


    public static void main(String[] args) {
        UserService userService =  new UserService();
        userService.login("kevin", "123456");
    }
}

In fact, many of the verification frameworks we usually use use such a principle, which decouples the permissions of each dimension and then concatenates them. Each is only responsible for its own relevant responsibilities. If the responsibility is not related to oneself, it will be thrown to the next Handler in the chain, commonly known as kick the ball.

1.2 combination of responsibility chain mode and builder mode

We can see that the previous code is in the UserService class. When the chain structure is long, the code is also bloated. If the business logic is modified later, it needs to be modified in the UserService class, which does not conform to the opening and closing principle. The reason for these problems is that the assembly of the chain structure is too complex, and for the creation of the structure, we can easily think of the builder mode. Using the builder mode, we can complete the automatic chain assembly of the processing node object specified by the UserService, only need to specify the processing node object, and do not care about anything else, and can specify the processing object node The chain structure is different with different order. To modify the Handler code:

public abstract class Handler<T> {

    protected final String ROLE_NAME = "administrator";

    protected Handler chain;

    public void next(Handler handler) {
        this.chain = handler;
    }

    public abstract void doHandle(User user);

    public static class Builer<T> {
        private Handler<T> head;
        private Handler<T> tail;

        public Builer<T> addHandler(Handler<T> handler) {
            if(this.head == null) {
                this.head = this.tail = handler;
                return this;
            }
            this.tail.next(handler);
            this.tail = handler;
            return this;
        }

        public Handler<T> build() {
            return this.head;
        }

    }

}

Then modify the UserService class:

public class UserService {

    public void login(String username, String password) {
        Handler validateHandler = new ValidateHandler();
        Handler loginHandler = new LoginHandler();
        Handler authHandler = new AuthHandler();

        Handler.Builer builer = new Handler.Builer();
        builer.addHandler(validateHandler)
                .addHandler(loginHandler)
                .addHandler(authHandler);

        builer.build().doHandle(new User(username, password));
    }


    public static void main(String[] args) {
        UserService userService =  new UserService();
        userService.login("kevin", "123456");
    }
}

2, The embodiment of responsibility chain pattern in source code

2.1 Filter class in Servlet

package javax.servlet;

import java.io.IOException;

public interface Filter {
    default void init(FilterConfig filterConfig) throws ServletException {
    }

    void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException;

    default void destroy() {
    }
}

The definition of this Filter interface is very simple, which is equivalent to the abstract role of Handler in the responsibility chain pattern. How does it form a responsibility chain? In the last parameter of the doFilter() method, we have seen the FilterChain class. Let's see the source code of this class:

public interface FilterChain {
    void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException;
}

Only one doFilter() method is defined in the FilterChain class. The specific logic is implemented by the user himself. Let's take a look at the MockFilterChain class implemented in Spring:

public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
    Assert.notNull(request, "Request must not be null");
    Assert.notNull(response, "Response must not be null");
    Assert.state(this.request == null, "This FilterChain has already been called!");
    if (this.iterator == null) {
        this.iterator = this.filters.iterator();
    }

    if (this.iterator.hasNext()) {
        Filter nextFilter = (Filter)this.iterator.next();
        nextFilter.doFilter(request, response, this);
    }

    this.request = request;
    this.response = response;
}

It puts all the filters in the chain into the List, and then iterates over the List when calling the doFilter() method, that is, the filters in the List will be executed in sequence.

2.2 Pipeline in netty

The Pipeline in Netty adopts the responsibility chain mode. The bottom layer adopts the data structure of two-way list, which connects the linked processors in series. For every request from the client, Netty thinks that all processors in the Pipeline have the opportunity to handle it. For the incoming requests, they all propagate from the beginning to the end, and the messages will not be released until they propagate to the end. Responsible for the source code of the processor's 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 is part of {@link ChannelInboundHandler}
     */
    @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 makes a more fine-grained division of the interface responsible for processing. There are two types of processors: one is the in stack processor ChannelInboundHandler, the other is the out stack processor ChannelOutboundHandler. Both interfaces inherit the ChannelHandler interface. All processing is added to the Pipeline. Therefore, the interface behavior of adding and deleting responsibility processors is specified in ChannelPipeline:

In the default implementation class, all handlers are strung into a linked list:

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 FastThreadLocal<Map<Class<?>, String>> nameCaches =
            new FastThreadLocal<Map<Class<?>, String>>() {
        @Override
        protected Map<Class<?>, String> initialValue() throws Exception {
            return new WeakHashMap<Class<?>, String>();
        }
    };

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

As long as we don't manually propagate down any node in the Pipeline, the event will stop propagating in the current node. For the data in the stack, it will be passed to the tail node for recycling by default. If we don't do the next propagation, the event will terminate at the current node. Writing the data back to the client also means the termination of the event.

3, Advantages and disadvantages of responsibility chain model

Advantage:

  • Decouple request and processing;
  • The request handler (node object) only needs to pay attention to the requests that he / she is interested in to process. For the requests that are not interested, he / she can directly transfer to the next level node object;
  • It has the function of chain transfer and processing request. The request sender does not need to know the link structure, only needs to wait for the request processing result;
  • The link structure is flexible and can be dynamically added or deleted by changing the link structure;
  • It is easy to extend the new request processing class, which conforms to the open close principle.

Disadvantages:

  • Too long responsibility chain or processing time will affect the overall performance;
  • If there is a circular reference to a node object, it will cause a dead cycle and cause the system to crash.

Keywords: Programming Netty Java Spring

Added by PHPfolife on Mon, 16 Mar 2020 12:22:36 +0200