What exactly is a workflow? Just a series of steps? If that is so, then software in general would seem to be a workflow. No, I’m thinking something a bit more specific than that.
Wikipedia defines Workflow as:
A workflow consists of a sequence of connected steps. It is a depiction of a sequence of operations, declared as work of a person, a group of persons, an organization of staff, or one or more simple or complex mechanisms.
When I say workflow I think of something like a document approval system. A document must make its way through to all the various steps/people until it reaches the final stage as which point the document is approved or denied. A workflow, to me, has to do people and how they work.
I’m really interested in how to handle workflows so that changes can be made in the future without having to alter larges portions of the application. Unfortunately, I usually see the workflow logic strewn throughout the entire application in which it lies. When change needs to happen, large number of files need to be altered thereby making it difficult to ensure nothing was missed and no errors were introduced. Encapsulating workflows in an object or series of objects goes a long way to easing changes and separating the workflow logic from other areas of the application.
So how do you go about this?
I’ve recently been inspired by two similar articles/presentations, both of which use a dependency injection framework to setup the workflows. One was given at CFObjective by Dan Skaggs called Building Advanced Workflows with ColdSpring. He presented on using ColdSpring to create a simple workflow system. The slides and code are on his website at the link above. If you haven’t taken a look at it I would strongly recommend you do. Very good stuff.
The second inspiration was an article at JavaWorld by Steve Dodge called Use Spring to Create a Simple Workflow Engine. While the previous example used ColdFusion and ColdSpring, this uses Java and Spring. It is however, fairly similar to the example above.
The premise with both is that each step is encapsulated in an object. Skaggs passes in a series of services straight into a "workflow processor" that executes a specified method on each object and passes in the initial data.
Dodge uses a slightly more complex approach by having "activity" objects that the services can be passed into and then the activities are passed into a workflow processor. This allows more fine grained control over the workflow. Each activity can stop the workflow, have a designated error handler, a designated logger, and more, all of which is great except for the fact that it adds more complexity.
What I’ve done is create my own implementation. Mine is close to Dodge’s example but I’m working on some changes to allow for a simpler approach when desired.
At the core of my implementation are Workflow Processors. These processors take an array of objects (activities) and then execute them depending on what kind of processor it is. Right now I’m working on two types: Sequence and Exclusive Choice. If you are wondering where I got these names, it is from here. Basically though, Sequence is simply a series of sequentially executed steps. Exclusive Choice is a little more complex in that it allows decisions to be made and then the necessary step executed. From "exclusive" in name you could probably guess it only allows for one choice. It is really just a glorified "if" statement.
Similar to each example, I use a dependency injection framework, ColdSpring in my case, to setup the workflow. Here is an example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 <bean class="WorkflowCF.Framework.Processors.Sequence" id="ApproveDocument">
<!-- Constructor Args: Required -->
<constructor -arg name="WorkflowName"><value>ApproveDocument</value></constructor>
<constructor -arg name="Activities">
<list>
<ref bean="CheckDocumentForMistakes" />
<ref bean="UpdateTheDocumentInTheDB" />
<ref bean="EmailTheDocument" />
</list>
</constructor>
<!-- Setter Args: Optional -->
<property name="DefaultErrorHandler"><ref bean="MyErrorHandler" /></property>
<property name="Logger"><ref bean="MyLogger" /></property>
</bean>
<!-- Activities -->
<bean class="CheckDocumentForMistakes" id="CheckDocumentForMistakes" />
<bean class="UpdateTheDocumentInTheDB" id="UpdateTheDocumentInTheDB" />
<bean class="EmailTheDocument" id="EmailTheDocument" />
What happens is that the Sequence processor is passed an array of activity objects to be executed sequentially. Activity objects are just the single steps in the chain. They must have an "execute" method as that is what is automatically called by the processor to run the activity.
In addition to activities, the processor can be passed an error handler object as well as a logger object. The error handler simply handles errors via a "handleError" method so you can handle them in custom ways. The logger simply implements the standard logging methods (info, debug, etc.) so you can see what goes on inside your processor. I’ve been using LogBox but you could certainly use any logger you like as long as it implemented the right methods.
The last bit of the puzzle is the Context object. The Context gets passed around between all the activities. When the Sequence is executed, a new Context is created and populated with the arguments. It is then passed to each activity as they are executed so each step can have access to the arguments. In addition, there are a few other functions it performs. First, activities are able to stop execution of the process by calling the "stopProcess" method on the Context. This could be useful where you want a set of steps to be executed but only it the previous steps execute correctly. The context also allows you to set messages to be returned from the processor. I’m not 100% certain I like this but right now I’ll keep it in there. It could be useful for error messages and the like. Finally, I allow data to be set into the context to be returned from the processor. This is another thing I’m not yet sure on. For now though, I just have a getWorkflowData and setWorkflowData that allows a struct to be set in the Context which will be returned at the end.
For now I won’t show all the the code but you can go check it out at my GitHub. It is only a few files right now so not very complicated. There are some things I didn’t cover like custom context objects and custom error handlers for activities so definitely take a look.
Handling workflows in this manner is still very new to me although it is definitely appealing. If nothing else I have a newfound respect for the power and flexibility of ColdSpring. The next thing I want to do in this project is implement the Exclusive Choice processor as well as simplified processors similar to Skagg’s for use in situations where the complexity of activity objects, contexts, etc. isn’t worth it. I also want to investigate some other options like async workflows. There are definitely situations where a "fire-and-forget" sort of workflow could be useful but I’ll need to look into it further. Overall though I’m quite pleased considering this is my first attempt to handle workflows like this.
I am now quite curious to know how other people handle workflows and how real world production workflows are handled. In my investigation there seems to be a number of "workflow frameworks" but they all appear fairly complex not to mentioned made for Java or .NET. This has been a fun experiment – one that I will hopefully be able to use it future code. I welcome any input on what I have so far as well as any guidance on this subject.
No related posts.
Posts