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.

18 comments:

Anonymous said...

Excellent addons. It's a shame that this plugin hasn't reached the general population to a higher degree. It really helps keeping the code clean and maintainable when working with models in workflows. Great work.

- Chetan Patil said...

Erik - Appreciate the feedback! :)

Glenn Rempe said...

Hi, Interesting post and additions.

I was introduced to acts_as_state machine as I think many others will be since it is now a supported part of the restful_authentication plugin (RA can generate a stateful user model and will use acts_as_state_machine).

I was sad to see though that aasm has not been updated by its author for more than a year. I was curious if you had been in touch with the original author to see about merging your changes into the master, or alternatively working with Rick Olsen to encourage use of your version? It would be great to see RA and AASM grow together.

Just a thought.

- Chetan Patil said...

Glenn - Thanks for the feedback. I will try to get in touch with Rick and see if he has any plans on AASM.

Anonymous said...

Excellent!

I would like to know the exact possible return values of any fired event! because aasm returns true when the event succeed, yours return false and when guard failed, the result is a StateTransition array.
So, I had previously updated the aasm to:
> def fire(record)
>.. next_states(record).each do |transition|
>.... break true if transition.perform(record)
>.. end # == true # I added the ' == true'
> end

but I'm a bit lost with yours. It would be marvelous to have the following return values:
- false : the event transition failed.
- true : the event transition succeeded.
- any other value : the event transition succeeded and returned the :on_transition callback value.

A final question: is there some more complete doc for aasm?

Greetings and thanks from
b u cc i n oo (a) m a c.c om

- Chetan Patil said...

Buccinoo - Thanks. I will look into the code and post an update soon. Also, I didn't find any special documentation on aasm other than what has been posted out there on blogs :(

- Chetan Patil said...

Buccinoo - I have emailed you an update.

Unknown said...

Chetan,
I should say that this is a really nice enhancement to the original plugin. I just had one more need. Is there any way to allow "multiple" states? I realize that this violates a state machine, but what if someone wants to do :dreaming while :sleeping. Is there any way to enhance it?

thanks a lot.

- Chetan Patil said...

railsnovice - As you said, it violates the state machine paradigm. Perhaps you should model dreaming_while_sleeping as a separate state if it is of importance to your state machine flow. Day-dreaming would be another state to consider then.
Another way to look at this is whether dreaming_while_sleeping is a state or an event. If you always dream (state) once you are in sleep (state), simply fire off the event of dreaming_while_sleeping which goes from the states sleeping to dreaming.

- Chetan Patil said...

I also meant to add a code snippet - not sure how it will show up though -

state :sleep, :enter => Proc.new {|o| o.dreaming_while_sleeping!}
state :dream

event :dreaming_while_sleeping do
transitions :to => :dream, :from => :sleep
end

Unknown said...

Chetan,
First off, thanks a lot for getting back to me so quickly. Second, I'll try to give you a situation that I am in (which I was paraphrasing as dreaming while sleeping).
I am trying to create an article model that allows for a workflow. The article table will contain a "published" state and also a "working" state.
Here are my states : working, submitted, published. Now, when a article gets submitted, obviously the transition will be to submitted, then upon approval it will be "published".
However, if someone tries to edit the blog article, I am trying to keep a "working" copy in the same table as "published". So, the author could be working on it while the published copy displays.
I am getting the feeling that the finite state machine is probably not for me then. What's your opinion?
Again, thanks.

- Chetan Patil said...

railsnovice - Here is my take: If you don't have any guard conditions (conditions that need to be satisfied before the object can be moved from one state to other) or any useful work that needs to be done as a side effect of a state change, a state machine is an overkill. In your case, as the article is submitted, is there an approval process that it needs to go through to be published? Or is it simply working->submitted->published?

Perhaps this might be easier: by default an article is in the working state. On submission, you set a column to indicate the article has been published. You don't need a state machine to implment this then. If you want versioning of the article, you can consider using acts_as_versioned.

Unknown said...

We have a workflow associated for approving/rejecting the blog article. But, like you suggested I looked into acts_as_versioned. Based on that, here's what I think I'll do:
Use acts_as_state_machine to keep only the different states of workflow (obviously one state per article): working, submitted, approved, published.
But, I'll also use acts_as_versioned to store all the versions, the latest one being the "published" one. So, as an article moves through workflow and reaches "published", it will slide off into the versioned table becoming live/published.
This allows me to have a workflow in one table and also lets me version all articles with the top most copy being the published one.

Again, thanks a lot for your help Chetan.
P.S. I know acts_as_state_machine could be an overkill, but I think I'll still use it as it provides a nice framework for my workflow, so I don't have maintain the workflow in my app.

Daniel Cadenas said...

The great thing about this plugin is that it let's you have a single declarative place where you can draw your state diagram in code. It doesn't get tangled with your behaviour in different places so your model gets simpler and that's always good.

Anonymous said...

Excellent addon! I missed just those features in aasm which you added with this addon. Great!

I have a strange problem though and maybe others experienced a similar behaviour?

If I switch to production mode, the addon doesn't seem to work. I figured out that setting
config.cache_classes = true
makes the difference.
So the reason seems to be that in the case of class-caching the module doesn't get bound.
I use rails 2.2.2

If I make a terrible stupid mistake, just remove this comment :-)

- Chetan Patil said...

@Daniel - Hmm, I have not used it with a Rails 2.2.2 project. Let me try it over the weekend and report back.

Anonymous said...

Hi Chetan
Did you find out somethings concerning Rails 2.2.2?
Daniel

- Chetan Patil said...

Hi Daniel,
I tried it out and it worked fine.
I have uploaded the rails project at
http://cpatil.googlepages.com/aasm_2.2.2.tgz
and a sample console session:

cpatil@chetan [~/test1 1057]-> script/console production
Loading production environment (Rails 2.2.2)
>> p = Person.new({:shirt_color => 'red', :trouser_type => 'dress pants'})
=> #<Person id: nil, shirt_color: "red", trouser_type: "dress pants", status: nil, created_at: nil, updated_at: nil>
>> p.save!
=> true
>> p.wakeup!(:next_state => 'working')
=> [true, nil]
>> p.current_state
=> :working
>> p.set_initial_state
=> "sleeping"
>> p.wakeup!(:next_state => 'showering')
=> [true, nil]
>> p.current_state
=> :showering
>> success, ret_val = p.dress!("blue", "jeans", :next_state => :working)
=> [true, 2]
>> p.current_state
=> :working
>> p.shirt_color
=> "blue"

Hope this helps.