Acts As State Machine

What the heck is a state machine and why should you use one? Wikipedia say:

A finite state machine or finite state automaton or simply a state machine, is a model of behavior composed of a finite number of states, transitions between those states, and actions.

Put simply, a finite state machine (FSM) is a design pattern for handling the progression of an object through a set of arbitrary “states” and the transitions between those states. This is a really useful concept whenever you have some data that moves through a workflow. Let’s look at the example of a comment on a blog entry. The “happy path” steps are as follows:

  • A user posts the comment
  • The comment is run through an spam filter
  • The comment is approved by the moderator
  • The comment is displayed on the blog


Here’s an example diagram showing the basic states and transitions:
state machine diagram

Here’s what this looks like in your rails model: (I’ve left out anything not directly relevant)

	class Comment < ActiveRecord::Base
	  acts_as_state_machine :initial => :new, :column => :state

	  # Rails 2.1 named scopes for finding posts based on their state
	  named_scope :approved, :conditions => {:state => 'approved'}
	  named_scope :ham, :conditions => {:state => 'ham'}

	  # ActiveRecord filter to send this comment off
	  # to the spam checker (method not pictured)
	  after_create :spam_check!

	  # States
	  state :new
	  state :spam
	  state :ham
	  state :approved

	  # Transition events
	  event :spam do
	    transitions :to => :spam, :from => [:posted, :ham]
	  end

	  event :ham do
	    transitions :to => :ham, :from => :posted
	  end

	  event :approve do
	    transitions :to => :approved, :from => [:ham, :spam]
	  end

	end

This all works pretty well. The spam checker (not shown) can call the methods spam! or ham! on the object to transition it to the next step. These methods are created by Comment.event, which is created by the “acts as” macro. On top of being a very clean way to express this functionality, there’s a lot of control over what states can transition to what states without a big mess of nested conditionals. The one thing I found to be really missing with this library was filters. There is a little support included for callbacks, but if found it to be limiting and rather ugly.

So I went ahead and hacked up acts_as_state_machine to add in the functionality I wanted. Now I can do this:

class Comment < ActiveRecord::Base
  #stuff from above example left out

  # if a user has already posted comments that have been approved, go ahead and
  # automatically approve this one.
  after_transition :to => :ham do
    approve! if self.class.approved.find(:first, :conditions => {:author_email => author_email})
  end

  # report incorrect responses to the spam filtering engine
  after_transition :from => :ham, :to => :spam, :call => :report_false_negative
  after_transition :from => :spam, :to => :ham, :call => :report_false_positive
end

Basically, I wanted to be able to specify filters based on the from and/or to states. There’s a lot that can be inferred by this information. For example, if we’re transitioning from ham to spam, that means the spam filter has falsely marked this comment as not spam. Surely, we should report this mistake back so we may get better results in the future. This is something that absolutely should be handled by the model, in my opinion, and this transition filter seems a great way to handle it.

Because I love you all so much, I’ve decided to include my modified version of this plugin. Enjoy.

Download Dave’s Modified acts_as_state_machine

2 thoughts on “Acts As State Machine

  1. can you do:
    after_transition :from => spam do

    end?

    or after_transition :from => foo, :call => :bar

  2. yes. it supports leaving out either the to or from. You can also supply an array of states to to, from, or both.

    after_transition :from => :foo, :to => [:bar, :baz] do … end

    You can also supply a conditional

    after_transition :from => :foo, :call => :bar, :if => Proc.new{ @foo.stuff? }

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>