Thursday, November 29, 2007

acts_as_state_machine enhancements

Download the plugin enhancement here. Drop it into the lib directory and do a require in the model.

A project I am working on required me to use a state machine to manage the state of a model. After downloading acts_as_state_machine plugin and I set it up as mentioned here.

Though I liked the way the plugin sets up the state machine, it seemed to lack some functionality for my usage:
  • To be able to define an override event that would allow the user to select the state the model should go to.
  • While performing the transition from one state to another state, perform some operation that is specific to the triad - event, : from, :to
Let me show you what I mean by extending the example mentioned here.

%> script/generate model Person shirt_color:string trouser_type:string status:string
%> rake db:migrate

%> emacs -nw app/models/person.rb

require 'acts_as_state_machine_overrides'

class Person < ActiveRecord::Base

acts_as_state_machine :initial => :sleeping, :column => 'status'
state :sleeping
state :showering
state :working
state :dating

event :shower do
transitions :from => :sleeping, :to => :showering
transitions :from => :working, :to => :showering
transitions :from => :dating, :to => :showering
end

event :work do
transitions :from => :showering, :to => :working
# Going to work before showering? Stinky.
transitions :from => :sleeping, :to => :working
end

event :date do
transitions :from => :showering, :to => :dating
end

event :sleep do
transitions :from => :showering, :to => :sleeping
transitions :from => :working, :to => :sleeping
transitions :from => :dating, :to => :sleeping
end

event :wakeup do
transitions :from => :sleeping, :to => [:showering, :working]
end

event :dress do
transitions :from => :showering, :to => [:working, :dating],
:on_transition => :wear_clothes
end

def wear_clothes(shirt_color, trouser_type)
self.shirt_color = shirt_color
self.trouser_type = trouser_type
end

end

Ok, I put in a lot of stuff in there to chew on. Lets discuss it one by one.

event :wakeup do
transitions :from => :sleeping, :to => [:showering, :working]
end
I can wake up and either go take a shower, or go sit on my computer and start working. This implies I have two transitions in the event wakeup:

a) from sleeping to showering
b) from sleeping to working

If I express this as

event :wakeup do
transitions :from => :sleeping, :to => :showering
transitions :from => :sleeping, :to => :working
end

%> script/console
>> p = Person.new({:shirt_color => 'red', :trouser_type => 'dress pants'})
>> p.save!
>> p.current_state
=> :sleeping
>> p.wakeup!
=> true
>> p.current_state
=> :showering

acts_as_state_machine simply picks up the first transition in this case. There is no way for me to wake up and start working, unless I create a new event for this transition and invoke it.

If you notice, these two transitions were already present in the example, but were fired when the user called work! or shower!. And that makes complete sense for this particular example where for all incoming transitions into a state are modeled as the event - i.e. all transitions that end up in the state :working are bundled in the event :work. I call this bottom up approach, where you create events based on incoming transitions for a state. In complex state machines however, you are not always free to choose the event based on the :to state, rather the events are modeled after real life actions that users perform. This is more like the top down approach, where you are creating events based on outgoing transitions from a state. Does that make sense? In any case, continuing on with this post.

So I extended acts_as_state_machine plugin to accept an array for the :to argument and allow the user to specify the next state to transition to when the event is fired.

event :wakeup do
transitions :from => :sleeping, :to => [:working, :showering]
end

%> script/console
>> p = Person.find(:all).last
>> p.set_initial_state
>> p.current_state
=> :sleeping
>> p.wakeup!(:next_state => 'working')
=> true
>> p.current_state
=> :working
>> p.set_initial_state
=> "sleeping"
>> p.wakeup!(:next_state => 'showering')
=> true
>> p.current_state
=> :showering
So now the user can be queried for the desired :to state.

Did you notice the extension to the event method. It now accepts the an optional hash argument as its last argument. Yup, I said - as the last argument- . But more on that in a minute.

So the new method signature is now event_name!(*args, [:next_state => ...]).

Next, we consider the issue of performing some work during the transition. The existing acts_as_state_machine plugin assumes any work that needs to be done would be done as you enter, after and exit each state. But with our new addition above of multiple possible :to states, there might be some common work that needs to be done that is not specific to the final :to state.
Case in point, after I take a shower, I need to wear clothes before either going to work or on a date. The act of wearing clothes does not fit in either going to work or going on a date.

I know what the counter-argument is going to be - :dress should be a state in itself and there should be a transition going from :showering to :dress and then transitions from :dress to :working and :dress to :dating. And then wearing the clothes should be done in the :dress state.

state :dress , :enter => :wear_clothes

event :dress do
transitions :from => :showering, :to => :dress
end
event :work do
transitions :from => :dress, :to => :working
...
end
event :work do
transitions :from => :dress, :to => :working
...
end
event :date do
transition :from => :dress, :to => :dating
...

Good point! I don't really have an answer to that yet, just a gut feeling that creating a virtual state for every little action that needs to be performed may lead to an unnecessarily complex state machine. If the work that needs to be performed is not related to the domain of the model which is being considered, it will introduce unrelated states in model's state machine.
I know from a purist pov all such logic should reside in the controller. But then you have the model domain logic creeping out into the controller where some actions may need to be performed based on the current state and the next state.
In any case, I implemented the on_transition feature for my project and here it is:

Similar to the :guard option for a transition, one can specify a :on_transition argument to the transitions call. The callback can be a symbol or a Proc. You can also specify arguments to the callback by specifying them when you fire the event. Oh, and the callback can also return a value back as shown below:

event :dress do
transitions :from => :showering, :to => [:working, :dating],
:on_transition => :wear_clothes
end

def wear_clothes(shirt_color, trouser_type)
self.shirt_color = shirt_color
self.trouser_type = trouser_type
id
end

%> script/console
>> p.current_state
=> :showering
>> success, ret_val = p.dress!("blue", "jeans", :next_state => :working)
=> [true, 10001]
>> p.current_state
=> :working
>> p.shirt_color
=> "blue"


The on_transition callback is called after the guard callback. The return value of the on_transition callback in no way affects the transition. It is simply considered to be a side effect of the trasition.

Oh and one more thing, the :from argument in the transitions call can be an array as well (this was part of the original acts_as_state_machine plugin). So you could do :

event :sleep do
transitions :from => [:showering, :working, :dating], :to => :sleeping, :guard => :brush_teeth
end

With the above enhancements, you can now do

event :dress do
transitions :from => [:showering, :sleeping], :to => [:working, :dating], :on_transition => :wear_clothes
Update: I forgot to mention these enhancements also include a method called next_events_for_current_state that I picked up from here, and fixed it to return the event names rather than the next states.

Note: I haven't tested this rigorously, so if you do find something amiss, drop me a note.

Njoy.