Workflow, part 3

I presented OOP approach for solving workflow tasks in my previous post. Now I'd like to show you, how to deal with workflows in a slight different manner. I'm gonna write about Workflow lbrary from CPAN. And I'm going to present you some kind of a tutorial on how to use this lib, and what features does it have.
The main idea of Workflow lib is configuration over coding. Flow is described in XML files, of course 'some' coding is still required.
So let's implement our request management using workflow lib. First of all, we'll create application, with all that simplifications, that we had in previous post, and than enhance it to real world web application and add some complex and interesting features that workflow lib can offer.

Ok, first of all we need to describe our flow.There will be 3 xml files.
First one is workflow_actions.xml

<actions>
    <type>Request Management</type>
    <action name="submit_request" class="Actions::Mail"/>
    <action name="approve_request" class="Actions::Mail"/>
    <action name="reject_request" class="Actions::Mail"/>
    <action name="complete_request" class="Actions::Mail"/>
</actions>

We have list of actions here. Each action has mandatory name and class attributes. You can think of class just as some code that should be done, when action will be triggered.
If you don't need any action in action (yep, it sounds strange) you can use Workflow::Action::Null, that does nothing. But why do we need action that does no action?
Well, the main purpose of action is to switch states (and this behavior is described in workflow.xml). And here we just have some additional code, that will be executed after state change.
So when you use Workflow::Action::Null you mean - don't execute additional code, just change the state.

In type tag - we provided workflow type name (since there can be multiple flows in one application). This name will be passed to workflow constructor later

use Workflow::Factory qw(FACTORY);
FACTORY->create_workflow('Request Management');

Now, let's check Actions::Mail. It should be inherited from 'Workflow::Action' and must implement execute method where all actual work will be done.

package Actions::Mail;

use v5.10;
use base 'Workflow::Action';

sub execute {
    my ( $self, $wf ) = @_;

    say 'Action name: '.$self->name;
    say 'Sending Email';

    return 1;
}

1;

In workflow.xml we will define states, how to navigate between states, and what actions to call

<workflow>
    <type>Request Management</type>
    <persister>RequestManagementPersister</persister>

    <state name='INITIAL'>
        <action name='submit_request' resulting_state='Submitted'/>
    </state>

    <state name='Submitted'>
        <action name='approve_request' resulting_state='Approved'/>
        <action name='reject_request' resulting_state='Rejected'/>
    </state>

    <state name='Rejected'>
    </state>

    <state name='Approved'>
        <action name='complete_request' resulting_state='Complete'/>
    </state>

    <state name='Complete'>
    </state>
</workflow>

First state - must be INITIAL. Other state names are not limited. Each state could have multiple actions (you specify action name here, which is from workflow_actions.xml and 'new' state that we want to have after action execution).

Also we define link to persister associated with this workflow. The job of a persister is to create, update and fetch the workflow object plus any data associated with the workflow. It also creates and fetches workflow history records.
Persister itself defined in workflow_persister.xml

<persisters>
    <persister name="RequestManagementPersister"
        class="Workflow::Persister::File"
        path="/home/ip/use.perl/workflow/persister"/>
</persisters>

We will use file system for storage, but you can choose DBI database if you like.

That's it. We defined or request management flow. And now let's create our Request object.

package Request;

use Mouse;
use Workflow::Factory qw(FACTORY);

has 'wf' => (
    is => 'rw'
);

sub BUILD
{
    my $self = shift;
    FACTORY->add_config_from_file( 
        workflow  => '/home/ip/use.perl/workflow/workflow.xml',
        action    => '/home/ip/use.perl/workflow/workflow_actions.xml',
        persister => '/home/ip/use.perl/workflow/workflow_persister.xml' 
    );

    my $wf = FACTORY->create_workflow('Request Management');
    $self->wf($wf);
}

no Mouse;
1;

It's easy. Just tell workflow lib where the xml are, and call create_workflow. Now let's check what we can do with that workflow object

#!/usr/bin/perl

use v5.10;
use strict;
use warnings;
use Request;

my $request = Request->new();

# $request->wf->_get_workflow_state returns Workflow::State object
# it has state property, where state name stored

# we can retrieve list of available actions in current state 
# $request->wf->get_current_actions()

say 'Current State: '.$request->wf->_get_workflow_state->state;
print 'Available actions: ';
foreach my $a ($request->wf->get_current_actions())
{
    print $a.' ';
}

# OUTPUT
#
# Current State: INITIAL
# Available actions: submit_request 



$request->wf->execute_action('submit_request');
say 'Current State: '.$request->wf->_get_workflow_state->state;
print 'Available actions: ';
foreach my $a ($request->wf->get_current_actions())
{
    print $a.' ';
}

# OUTPUT
#
# Action name: submit_request
# Sending Email
# Current State: Submitted
# Available actions: approve_request reject_request 

$request->wf->execute_action('reject_request');
say 'Current State: '.$request->wf->_get_workflow_state->state;
print 'Available actions: ';
foreach my $a ($request->wf->get_current_actions())
{
    print $a.' ';
}

# OUTPUT
#
# Action name: reject_request
# Sending Email
# Current State: Rejected
# Available actions: 

That's it for today. In next post, we'll extend our simplified request management, into real world web application.
Stay tuned.
Notes and comments are welcome.

4 Comments

Been a great read so far, plan to share this with work colleagues that don't always keep up with Perl blogs. Just two things:

1) "_get_workflow_state" that underscore is conventionally meaning 'private' and I'd not think to access it directly in the way you have in this example. Is this just 'we made it private but actually is ok to use' or are you showing it this way because it is easier to understand for now but actually there's a better way?

If this is sorta private I wonder if using Moose/Moo attribute delegation might not be a good way to hide the encapsulation? Like:

has 'wf' => (
is => 'rw'
handles => {
current_state => '_get_workflow_state',
},
);

Then you could go "$request->current_state". Sometimes this is a good approach but overdoing it can cause confusing APIs as well. In this case your Request case does seem like a true adaptor for Workflow so might be ok.

2) When this is done would enjoy to hear your opinion on another Workflow class on CPAN that rates as reasonable popular, "Class-Workflow" which seems to prefer code over configuration. That approach can fit a certain type of programmer better than XML, which some people loath :)

Thanks again!
John

OK - configuration files should be readable, because they need to be understood and maybe even changed by non-coders. To follow this argument to it's extremum you'd adjust the configuration format to the audience i.e. use what would be most readable for them, maybe they would prefer simple .ini files or maybe some YAML or whatever - and there is a CPAN module that would let you do that: Config::Any :)


My favourite usage pattern of this is to use Config::Any::Perl for the start - when your audience is mostly you and yourself (i.e. programmers) - and later migrate to XML or .INI or something else.

Very good set of posts, was hoping to see the next in your series!

Leave a comment

About Ivan Paponov

user-pic Yet Another Perl Blogger