Perspectives

Acts_as_state_machine

When mapping the flow of an application, we usually have various states for a single object. These states are then joined with lines showing how an object can transition from one state to another. Each state will be unique; it may be just simply have a status flag, or it could open up new functionality within the app. For example, an e-commerce application may have a system for package tracking. When customer pays for a product, that product will have a status of 'paid'. Based on the paid status, the seller will be notified that the product needs to be shipped and the customer will gain the ability to write a review.

Traditionally, we would have to create methods that dealt with switching and checking states. Thankfully that has all changed with the wonderful ActsAsStateMachine plugin. What I like best about ActsAsStateMachine is that it has its own methods in handling states and event transitions that won't get lost with other methods within the model. I will go more into detail soon, but first we need to create a quick application to showcase the awesomeness of ActsAsStateMachine!

Let's create a baseball application that will display scores of current games. Each game will be in the database before it starts, allowing users to view if tickets are available. When the game is live, ticket information will not be needed and a scoreboard with all the innings will be displayed instead. When the game is over, only the final score will appear with information for the next game.

To get started, first install the plugin:

ruby script/plugin install http://elitists.textdriven.com/svn/plugins/acts_as_state_machine/trunk/

Then within our Game model, add the following:

acts_as_state_machine, :column => :game_state, :initial => :pending

Here we are simply telling the Game model to act as a finite state machine. We are setting the column that will hold the Game's state as game_state. By default, ActsAsStateMachine will use the state column. This can cause problems since a state column in most applications will hold location information. So by specifying a column name, we can avoid column problems. The last portion will set the initial state of a newly created Game object to pending. It is similar to creating a migration and setting the column to a default value, but if that default value ever needs to change, a new migration would have to be created. By setting a initial value, we can easily change just that one line. Now lets define all the various states for a game. Based on the app's requirements, a game will have three states: Pending, Live and Final. Defining states is as easy as adding:

state :pending
state :live
state :final

Our Game model now has states and with each state definition new methods are created. These methods may come in handy for state-specific validations or can be used in conditionals within the views that will load certain partials. For example, instead of doing @game.state == "live", we can use @game.live?. With all the states for Game defined, it is time to add events that will transition the game from one state to another. ActsAsStateMachine has it's own methods for creating events, which is basically a loop. We start by defining the event loop, giving the event a name and then specifying transitions within.

event :start_game do
  transitions :from => :pending, :to => :live
end

event :end_game do
  transitions :from => :live, :to => :final
end

With these events we can now transition a game object's state by using @game.start_game! and @game.end_game!. Keep in mind each event can hold many transitions, we can combine the two event methods into one as long as the :from portion is different. It wouldn't make much sense in the context of our example, however, because it is linear.

We have had the app up for a few weeks now and baseball fans are really digging the slick up-to-the-minute scores that we update through AJAX calls. Eventually we will run into our first rain delay, which will lead to problems. Fans will start thinking the app has stopped updating and our servers will keep running expensive AJAX updates over a game that has turned idle. It is time to introduce the rain delay state and event, making our simple app less linear.

state :rain_delay
  
event :rainout do
  transitions :from => :live, :to => :rain_delay
end

With a rain delay, a choice will be made by the umpires. If enough innings have been played, the game can end with the current score being final. Otherwise the game can be canceled and be rescheduled at a later date. Let's add the new canceled state and transitions events from rain_delay. Lets also add a new transition into the end_game event for rain_delay games that have played enough innings to be called final.

state :canceled
  
event :end_game do
  transitions :from => :live, :to => :final
  transitions :from => :rain_delay, :to => :final
end

event :cancel_game do
  transitions :from => :rain_delay, :to => :canceled
end

Let's say if a game is canceled it is still in our database for historical purposes and a new game will be created with some data from the canceled game. Let's also say that the reschedule_game method will create a new game from a canceled game. ActsAsStateMachine lets us attach callbacks like state events. Basically we want to run the reschedule_game method whenever a game's state changes to canceled. To accomplish this, we simply add :enter => methodname after defining the state as so:

state :canceled, :enter => :reschedule_game

Now whenever a game's state switches to canceled, a copy will automatically be made through the reschedule_game method. Besides enter, ActsAsStateMachine also has after and exit options. After would run after the state switch has been made and exit will be executed when the object is transitioning away from the state.

ActsAsStateMachine comes in handy when a object's status goes through many changes. I hope I have shown you how easy it is to define states, create transitions and implement ActsAsStateMachine into a model. Here is what our final code looks like:

acts_as_state_machine, :column => :game_state, :initial => :pending

# States
state :pending
state :live
state :final
state :rain_delay
state :canceled, :enter => :reschedule_game

# Transition Events
event :start_game do
  transitions :from => :pending, :to => :live
end

event :end_game do
  transitions :from => :live, :to => :final
  transitions :from => :rain_delay, :to => :final
end

event :rainout do
  transitions :from => :live, :to => :rain_delay
end

event :cancel_game do
  transitions :from => :rain_delay, :to => :canceled
end




RSS Feed


CATEGORIES


ARCHIVES


BOOKMARKED


Add to Technorati Favorites