Design Pattern: the Pipeline
08/07/2012 2 Comments
Today we’ll have a look into the Pipeline pattern, a design pattern inspired from the original Chain of Responsibility pattern by the GoF
I The Chain Of Responsibility
Basically the Chain of Responsibility defines the following actors:
- Command: the object to be processed
- Handler: an object handling interface. There can be many handlers in the chain. This interface defines 2 methods
- setSuccessor(Handler successor): defines the next successor in the chain to pass the command to
- handle(Command command): handle the command
The goal of this pattern is to decouple the Command object from its processing chain. A new Handler can be easily added to the chain without breaking any existing code.
The disadvantage of this design is the number of operations needed when a new handler is added into the pipeline. You need to break an existing successor/predecessor link, insert the new handler then relink it to the chain. It is very similar to a linked list. If a successor link is badly set accidentally, the whole chain is broken.
In the context of dependency injection within a container (JEE or Spring) it is not easier either. 3 operations are required:
- unlinking an existing handler
- relinking the new handler with its successor
- relinking the existing predecessor to the new handler
All this is quite error-prone indeed.
II The Pipeline
The original need for the Pipeline pattern comes from my Tatami project.
When an user posts a new Tweet, it needs to be processed by several services:
- contentValidationService: to limit the tweet size to N characters, including the URL shortening
- xssEncodingService: encode the tweet content to prevent XSS (cross-site scripting) attacks
- tweetService: simply persist the tweet in the repository
- userlineService: put the tweet in the current user line
- timelineService: put the tweet in the current user timeline
- taglineService: put the tweet in the appropriate taglines if it contains tags
- mentionlineService: put the tweet in the timeline of any mentioned users
- contactsService: spread the tweet to the timeline of all followers of current user
As you can see, the tweet processing services resemble quite a lot to a pipeline of processors. And more importantly the processing order does really matter. xssEncodingService should be placed before the tweetService (responsible of the persistence in repository). Similarly, the tweetService needs to be placed before any line related service because in a multi-threaded environment, the tweet needs to be persisted before any user could read it.
The Pipeline pattern meet those requirements but is more flexible than the Chain Of Responsibility pattern. It defines the following actors:
- Command: command object to be process. A tweet in our example
- PipelineManager: central class of the pattern. Define the following attribute and method
- handlers: list (ordered) of Handlers
- doPipeline(Command object): execute the processing pipeline
- Handler: an interface defining the process(Command object) method
Below is the formal UML class diagram:
The major difference with the Chain Of Responsibility pattern is the introduction of the PipelineManager actor. The flexibility of the Pipeline pattern comes from the fact that at any time, a new Handler can be injected into the pipeline through the PipelineManager
Below is a sample Spring XML configuration illustrating dependency injection
<bean id="tweetPipelineManager" class="fr.ippon.tatami.service.pipeline.tweet.TweetPipelineManager"> <property name="tweetHandlers"> <list> <ref bean="tweetContentValidationService"/> <ref bean="xssEncodingService"/> <ref bean="tweetService"/> <ref bean="userlineService"/> <ref bean="timelineService"/> <ref bean="taglineService"/> <ref bean="mentionlineService"/> <ref bean="contactsService"/> <ref bean="retweetService"/> </list> </property> </bean>
Adding a new tweetHandler is as easy as adding a new entry in the list of handlers. Furthermore, since the handlers collection is a List, the order is preserved.