Strange Symphonies The whole is greater than the sum of its parts

24May/077

Ruby on Rails Finite State Machine Plugin: acts_as_state_machine

Update: Workflow is an alternative Finite State Machine Plugin for Ruby on Rails is available. I recommend you go give Workflow a try.

A finite state machine is a model of behavior with a finite number of states, interconnected via transitions and events.

In this guide, I will introduce you to a Ruby on Rails plugin to easily recreate a Finite State Machine with your models. The plugin we will be using is the acts_as_state_machine, which as you can see has very sparse documentation. Google does pull up some results, but not good enough, at least for beginners.

Why would you use a Finite State Machine?

For starters, if your model has a finite number of various states, and you want an easy way for callbacks to be done. Callbacks can be used to notify, validate, increment, anything, when your model changes state.

Installing acts_as_state_machine

Go to the root folder of your Rails application and execute:

./script/plugin install \

http://elitists.textdriven.com/svn/plugins/acts_as_state_machine/trunk/

Warning: Split over two lines as it was really long.

Note: Don't forget if your project is under subversion, you can use the -x flag to externally link to the plugin

Using acts_as_state_machine

class Person < ActiveRecord::Base
  acts_as_state_machine :initial => :sleeping
  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
end

Note: The plugin makes an assumption that the state of your model is saved in field called state. This can be replaced by adding the additional option :column => 'field'.

Warning: If you are using a model that stores addresses, be weary of a field called "state". You can spend hours wondering why things aren't working like they should be.

Note: Personal Preference: I like to describe the states my models are in with as an adjective, and the event as a verb.

Notice how in line 2 we explicitly state the initial state of the model. In lines 3 to 6, we indicate the various states the Person may be in.

Note: There is a peculiar behavior when creating objects via new, in that the model's state is not specified. It will only be specified when saving the new record. One solution is to specify the default state from within the migration. The other solution then is to call create.

Example:

person = Person.new
person.state # nil
person.save # true
person.state # "sleeping"

person = Person.create
person.state # "sleeping"
person.sleeping? # true
person.rotting? # false

person = Person.new
person.state = "rotting" # "rotting"
person.rotting? # true
person.sleeping? # false

Note: If you didn't notice the trend, the method to test if the model is in the state, is to append the state with a question mark: "state?"

The events you specified also creates instance methods, to transition the model from one state to another.

The following instance methods were created:

person.shower!
person.work!
person.date!
person.sleep!

Note: The instance methods created follow the pattern "event!"

Events

Note: By calling any event, you also call ActiveRecord::Base.save. For when it fails, it only returns false. You can guard yourself by calling valid? and save!

Events help you to transition from one state to another. So suppose your person is sleeping, and you want him to shower, well we'll just call shower!.

person.state # "sleeping"
person.shower!
person.state # "showering"

Events can help your organize the flow of your model. But they can get more powerful with callbacks.

Callbacks

The state also comes with a few callbacks that can be used.

state :sleeping,
         :enter => :get_into_bed,
         :after => Proc.new {|model| model.whack_alarm_clock },
         :exit => :make_up_bed

Callbacks are called when the model is transitioning into the specified state.

Note:

  • Callbacks can be either a symbol or a Proc. If used as a symbol, the instance method of the model will be called
  • The callbacks act differently if the model is a new record and hasn't been saved, versus an already saved model.

When put into consideration with ActiveRecord's callbacks, a new record's callback would look like this:

  • ActiveRecord::Base.before_save
  • ActiveRecord::Base.save
  • acts_as_state_machine :enter sleeping
  • acts_as_state_machine :after sleeping
  • ActiveRecord::Base.after_save

When the model is no longer a new record, the callbacks execute as follows, if I had called the shower! method.

  • acts_as_state_machine :enter showering
  • ActiveRecord::Base.before_save
  • ActiveRecord::Base.save
  • ActiveRecord::Base.after_save
  • acts_as_state_machine :after showering
  • acts_as_state_machine :exit sleeping

Guarding States

But how about if you want some sort of validation for a transition. You know, just to ensure data integrity.

  event :work do
    transitions :from => :showering, :to => :working
    # Going to work before showering?  Stinky.
    transitions :from => :sleeping, :to => :working, :guard => Proc.new {|o| o.clean? }
  end

The transition can be guarded by specifying a :guard option, with either a symbol or Proc (similar to the Callbacks). The method or Proc has to return true to proceed with the transition, else it will fail silently.

Conclusion

Well thats the basics, and as much as acts_as_state_machine does.

Any tips, tricks and or pointers? Leave them as comments.

Or are there any other interesting plugins you'd like me to explore?

Update: Workflow is an alternative Finite State Machine Plugin for Ruby on Rails is available. I recommend you go give Workflow a try.