1. Introduction

In this article, we’re going to take a look at a widely used behavioral design pattern: Chain of Responsibility.

We can find more design patterns in our previous article.

2. Chain of Responsibility

Wikipedia defines Chain of Responsibility as a design pattern consisting of “a source of command objects and a series of processing objects”.

Each processing object in the chain is responsible for a certain type of command, and the processing is done, it forwards the command to the next processor in the chain.

The Chain of Responsibility pattern is handy for:

  • Decoupling a sender and receiver of a command
  • Picking a processing strategy at processing-time

So, let’s see a simple example of the pattern.

3. Example

We’re going to use Chain of Responsibility to create a chain for handling authentication requests.

So, the input authentication provider will be the command, and each authentication processor will be a separate processor object.

Let’s first create an abstract base class for our processors:

public abstract class AuthenticationProcessor {

    public AuthenticationProcessor nextProcessor;
    
    // standard constructors

    public abstract boolean isAuthorized(AuthenticationProvider authProvider);
}

Next, let’s create concrete processors which extend AuthenticationProcessor:

public class OAuthProcessor extends AuthenticationProcessor {

    public OAuthProcessor(AuthenticationProcessor nextProcessor) {
        super(nextProcessor);
    }

    @Override
    public boolean isAuthorized(AuthenticationProvider authProvider) {
        if (authProvider instanceof OAuthTokenProvider) {
            return true;
        } else if (nextProcessor != null) {
            return nextProcessor.isAuthorized(authProvider);
        }
        
        return false;
    }
}
public class UsernamePasswordProcessor extends AuthenticationProcessor {

    public UsernamePasswordProcessor(AuthenticationProcessor nextProcessor) {
        super(nextProcessor);
    }

    @Override
    public boolean isAuthorized(AuthenticationProvider authProvider) {
        if (authProvider instanceof UsernamePasswordProvider) {
            return true;
        } else if (nextProcessor != null) {
            return nextProcessor.isAuthorized(authProvider);
        }
    return false;
    }
}

Here, we created two concrete processors for our incoming authorization requests: UsernamePasswordProcessor and OAuthProcessor.

For each one, we overrode the isAuthorized method.

Now let’s create a couple of tests:

public class ChainOfResponsibilityTest {

    private static AuthenticationProcessor getChainOfAuthProcessor() {
        AuthenticationProcessor oAuthProcessor = new OAuthProcessor(null);
        return new UsernamePasswordProcessor(oAuthProcessor);
    }

    @Test
    public void givenOAuthProvider_whenCheckingAuthorized_thenSuccess() {
        AuthenticationProcessor authProcessorChain = getChainOfAuthProcessor();
        assertTrue(authProcessorChain.isAuthorized(new OAuthTokenProvider()));
    }

    @Test
    public void givenSamlProvider_whenCheckingAuthorized_thenSuccess() {
        AuthenticationProcessor authProcessorChain = getChainOfAuthProcessor();
 
        assertFalse(authProcessorChain.isAuthorized(new SamlTokenProvider()));
    }
}

The example above creates a chain of authentication processors: UsernamePasswordProcessor -> OAuthProcessor. In the first test, the authorization succeeds, and in the other, it fails.

First, UsernamePasswordProcessor checks to see if the authentication provider is an instance of UsernamePasswordProvider.

Not being the expected input, UsernamePasswordProcessor delegates to OAuthProcessor.

Last, the OAuthProcessor processes the command. In the first test, there is a match and the test passes. In the second, there are no more processors in the chain, and, as a result, the test fails.

4. Implementation Principles

We need to keep few important principles in mind while implementing Chain of Responsibility:

  • Each processor in the chain will have its implementation for processing a command
    • In our example above, all processors have their implementation of isAuthorized
  • Every processor in the chain should have reference to the next processor
    • Above, UsernamePasswordProcessor delegates to OAuthProcessor
  • Each processor is responsible for delegating to the next processor so beware of dropped commands
    • Again in our example, if the command is an instance of SamlProvider then the request may not get processed and will be unauthorized
  • Processors should not form a recursive cycle
    • In our example, we don’t have a cycle in our chain: UsernamePasswordProcessor -> OAuthProcessor. But, if we explicitly set UsernamePasswordProcessor as next processor of OAuthProcessor, then we end up with a cycle in our chain: UsernamePasswordProcessor -> OAuthProcessor -> UsernamePasswordProcessor. Taking the next processor in the constructor can help with this
  • Only one processor in the chain handles a given command
    • In our example, if an incoming command contains an instance of OAuthTokenProvider, then only OAuthProcessor will handle the command

5. Usage in the Real World

In the Java world, we benefit from Chain of Responsibility every day. One such classic example is Servlet Filters in Java that allow multiple filters to process an HTTP request. Though in that case, each filter invokes the chain instead of the next filter.

Let’s take a look at the code snippet below for better understanding of this pattern in Servlet Filters**:**

public class CustomFilter implements Filter {

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

        // process the request

        // pass the request (i.e. the command) along the filter chain
        chain.doFilter(request, response);
    }
}

As seen in the code snippet above, we need to invoke FilterChain‘s doFilter method in order to pass the request on to next processor in the chain.

6. Disadvantages

And now that we’ve seen how interesting Chain of Responsibility is, let’s keep in mind some drawbacks:

  • Mostly, it can get broken easily:
    • if a processor fails to call the next processor, the command gets dropped
    • if a processor calls the wrong processor, it can lead to a cycle
  • It can create deep stack traces, which can affect performance
  • It can lead to duplicate code across processors, increasing maintenance

7. Conclusion

In this article, we talked about Chain of Responsibility and its strengths and weaknesses with the help of a chain to authorize incoming authentication requests.

And, as always, the source code can be found over on GitHub.


« 上一篇: Unirest指南
» 下一篇: Java Weekly, 第220期