Modernized Pattern Implementation With CDI: The Maximal Decoupled Virtual Chain of Responsibility
A modernized version of the Chain of Responsibility pattern for writing systems and code using the SOLID principles.
Join the DZone community and get the full member experience.
Join For FreeIn this part, I will show you a modernized, CDI-based version of the good old “Chain Of Responsibility” (CoR) pattern. As you will see, it is a perfect weapon if you want to design a system and write code that follows Uncle Bob’s “SOLID Principles”, especially the Open/Closed Principle, which is often one of the hardest to achieve.
The new design is based on an Interceptor that controls and performs the calls to the Chain Handlers, which have to be annotated with appropriate CDI-Qualifiers. The key difference to the usual Chain-Pattern is, that the chain handlers are not physically connected, i.e. a handler doesn’t know about any successor. Instead, the handlers are “virtually” connected by sharing a custom, Usecase-specific CDI-Qualifier, thus I would prefer to call this modernized version a “Virtual Chain Of Responsibility“.
Compared to the modernized factory pattern, which I had shown in part 1 (see either the original post or the DZone post), this CoR-design leads to an even more decoupled system, because you do not need to implement a factory anymore. Instead, it gets replaced by the generically implemented “ChainOfResponsibilityControllerInterceptor”. For an opensource implementation of this concept, see the section about the “magic4j-cdi-utils” library at the end of this article or visit magic4j.org.
How Does It Work?
The following UML-class diagram visualizes the key parts of the rewritten example from the part 1 blog about the factory-method pattern. If you are interested in the detailed design of the XML-Structure, then please find it in part 1.
Modernized “Chain Of Responsibility” Pattern – Example Classes
Here you can see an Interface that is implemented by 3 classes. All annotations are visualized by stereotypes. Please note, that the Usecase-specific Qualifier-annotation must be made at method level and not at class level (see the code snippets below for the details).
The first implementation on the left side is the so-called CoR-”Controller” to which the interceptor gets attached. This is also the class, that the client / caller needs to inject. The other two implementations are the specific Chain-Handlers: one for the validation of a Football-related MatchResult and one for a Tennis-related MatchResult. You might have many more implementation types in your real projects.
As soon as the client invokes the controllers’ validation-method, the interceptor can start it’s business:
First, the Controller-Interceptor detects all corresponding chain handlers by searching for beans, that have the @ChainOfResponsibilityHandler annotation at class level and the same custom Qualifier annotation as the Controller-method at method-level. Then it starts to call one after the other, until one “chain handler” finds itself responsible for the given input. That handler immediately performs the processing of the input data and signals to the controller, that no subsequent handler needs to be called anymore by returning a non-empty Java8-Optional. If an invoked handler finds itself not being responsible, it just returns an empty Java8-Optional.
Code Snippets of the Example
public interface OptionalMatchResultValidator {
/**
* Indicates if this implementation was responsible for the given
* MatchResult (see below).
*
* @param teamMatchResult
* @return an empty Optional if no validation was performed because this
* concrete Validator-Implementation was not responsible for the given kind
* of MatchResult. Otherwise an Instance of Response must be present in the
* Optional.
*/
Optional<Response> validateMatchResult(MatchResult teamMatchResult);
}
Listing 1: The example Business Interface for the Chain-Handlers and the Chain-Controller
Please note, that the interface method currently must return an Optional. The contract is, that if the returned Optional is empty the Interceptor must invoke the next chain handler. As soon as one Chain Handler finds itself responsible for the given input, it must return an Optional with an Instance. By the way, I personally find it convenient to return a “Response” Object that acts as a container for positive and negative results: It either contains the real outcome of the method or error messages in case of problems. The main reason for the Optional is that you can integrate the method into Java 8 call chains.
@ApplicationScoped
public class MatchResultValidatorChainController implements OptionalMatchResultValidator {
@Override
@ChainOfResponsibilityController
@MatchResultValidator
public Optional<Response> validateMatchResult(MatchResult teamMatchResult) {
throw new IllegalStateException("Method-Body of a Chain-Controller should not have been invoked!");
}
}
Listing 2: The Chain-Controller with the @ChainOfResponsibilityController Annotation
The Usecase-specific custom CDI-Qualifier is the @MatchResultValidator annotation. It needs to be present at method level. Secondly, you need to add the @ChainOfResponsibilityController-Annotation for applying the Interceptor to the method. This annotation is provided by the magic4j-cdi-utils library.
Please note, that the Interceptor doesn’t even call the method body of the intercepted Chain-Controller, because this would just lead to an ambiguity concerning the question which result the interceptor should provide to the client: Of course it must always return the result from the responsible chain handler and not the one from its own method body. That’s why this method throws an IllegalStateException if it is really called.
@ApplicationScoped
@ChainOfResponsibilityHandler
public class FootballOptionalMatchResultValidator implements OptionalMatchResultValidator {
@Override
@MatchResultValidator
public Optional validateMatchResult(MatchResult teamMatchResult) {
assertMatchResultNotNull(teamMatchResult);
if (isFootballMatchResult(teamMatchResult)) {
return doValidateFootballMatchResult(teamMatchResult);
}
return Optional.empty();
}
...
}
Listing 3: The Chain-Handler for the Football-related MatchResult.
At class level, you need the @ChainOfResponsibilityHandler-Annotation which is also provided my the magic4-j-cdi-utils library. At method level, you need again the custom Usecase-specific CDI-Qualifier, here @MatchResultValidator. It is the task of each handler to detect, if the given input is addressed to him. In part 1, this detection was done centrally in the factory, which is a disadvantage compared to this modernized Chain-Pattern.
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Target;
import javax.inject.Qualifier;
/**
* @author stephan
*/
@Qualifier
@Retention(RUNTIME)
@Target({
METHOD, FIELD, PARAMETER, TYPE
})
public @interface MatchResultValidator {
}
Listing 4: The custom Usecase-specific CDI-Qualifier for the example.
What’s the Difference to the CDI-built-in Decorator-Pattern?
The CDI-built-in Decorator-Pattern is suitable, if you have “n” implementations of an interface and you want them all to be invoked in every case. As a side note: In contrast to the Decorator, where all actual decorators need to be listed in the beans.xml file, the magic4j-CoR-Interceptor detects all fitting Chain Handlers at runtime, which makes CoR also easier to use. You only need to add the ChainOfResponsibilityControllerInterceptor to the beans.xml file of the module that contains the CoR-Controller.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
bean-discovery-mode="annotated">
<interceptors>
<class>org.magic4j.cdiutils.chainofresponsibility.ChainOfResponsibilityControllerInterceptor</class>
</interceptors>
</beans>
Why Does the CoR-design Fit Perfectly to the SOLID Principles?
- Single Responsibility Principle: You can define one interface with one method per Usecase (example Usecase here “Validate MatchResult”) and have one implementation per kind of sports. This fits perfectly!
- Open/Closed Principle: Your Software should be open for extension but closed for modification: In our example: When a new type of sport is added, you do not have to change any existing code: no new else-branches in existing if-else-blocks and/or no new case-branches in existing switch-case-blocks. Instead you simply add a new implementation of the Usecase-specific interface. If it correctly annotated, the Interceptor will find it automatically at runtime. Each implementation can even be put into a separate module so that you do not even have to touch existing modules (this is the same as shown in the component diagram of the factory-method pattern in part1, which applies here as well).
- Liskov’s Substitution Principle: This principle does not primarily apply to our example because we assume, that we do not need an inheritance hierarchy that is deeper than two levels (interface and implementation). In case you have similar types of MatchResults, you would have to create common parent classes for those similar types in a common package or module. But this can lead quite fast to a violation of the Open-Closed-Principle, so you should examine carefully if it’s worth it. Imagine a third similar type of sport is added and it needs a slight modification in the base class, then you get a violation of the Open-Closed-Principle. In other words, creating common parent classed can lead to a clash between the Open-Closed- and the DRY-Principle which you have to balance reasonably from case to case.
- Interface Segregation Principle: It tells you to avoid big interfaces with many methods. This is already implicitly achieved, if you do it as proposed under item a).
- Dependency Inversion Principle: The Usecase-specific interface represents the abstraction that is required by this principle to decouple higher-level- from lower-level components.
Advantages to the “Maximal Decoupled Parameterized Factory Method”-pattern From Part 1?
My article about the modernized factory-method pattern resulted in an interesting discussion concerning the question if that factory is in fact so well decoupled as I explained it. The critics was, that the factory still has some kind of knowledge about the different implementations:
- It depends on specific CDI-Qualifiers per implementation type
- It has knowledge about the structure of the incoming XML because it needs to decide which injection point to use based on structural information in the XML.
This results also in the fact, that the factory needs to be modified e.g. when a new implementation type is added. Even though I still believe that this kind of factory still does a much better job in decoupling the high-level logic from the low-level implementations (as explained in detail in my replies to the comments) than usual factories, the Chain-Of-Responsibility as shown here does it even better. You only need one custom CDI-Qualifier per Usecase (instead of one per implementation) and there is no central place anymore, that needs knowledge about the structure of the input data. This knowledge has been split into the diverse chain handlers, where each one only needs to know about his kind of structure. Nevertheless you should be aware that you trade the explicit coding of the factory against some magic that happens under the covers of the “CoR-Interceptor”.
Available as New Feature in the magic4j-cdi-utils Library
I provided a reference implementation of this modernized Chain-Of-Responsibility pattern in my magic4j-cdi-utils library, version 1.1.0.0. The library is open source under the Apache 2 license. You can download it from the maven central repository via the following dependency:
<dependency>
<groupId>org.magic4j</groupId>
<artifactId>magic4j-cdi-utils</artifactId>
<version>1.1.0.0</version>
</dependency>
Published at DZone with permission of Stephan Bauer, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments