Simple state machine example

I have been investigating using the ruby state_machine gem for a project. State machines can be very handy in simplifying code where objects move through various states. The state_machine gem embeds state machine behavior into any class that uses it. My first test was to create a simple todo list example. The todo list will include a main list that contains items and sublists. This allows a way to organize items easily into sublists.

A simple way to do this is to declare two classes List and Item and allow an object of class List to contain a list of Lists and a list of Items. This approach allows a flat list with many items, or a hierarchical list with top-level items, but also sub-lists that contain their own items.

In addition to lists and items, I want is to track whether I have completed all of the items in a list. In the case of a hierarchical list this means all items of sub-lists as well. Thus in addition to lists and items, we need to track whether items and lists are in progress or completed.

The next step is deciding on the states of the two object types. For my example, I decided: * a list will have an initial state of available, and will transition to state started when the first item on the list is completed. When all items and sublists are completed the list will move to the state completed. * an item will have an initial state of available and will transition to completed when the item is finished.

Finally I want to inform the user when: * an item is completed * a list is completed * the project is completed (all items and sublists are completed)

Here is a quick example of the code to meet these requirements:

list_example.rblink
require 'rubygems'
require 'state_machine'
class Item
attr_accessor :name, :description
state_machine :item_state, :initial => :available do
after_transition :available=> :completed, :do => :item_completed
event :finish do
transition [:available] => :completed
end
state :completed do
end
end
def initialize(item_name, item_description, parent = nil)
@name = item_name
@description = item_description
@parent_list = parent
super() # NOTE: This *must* be called, otherwise states won't get initialized
end
def item_completed
puts "finished item #{self.name}"
if @parent_list
@parent_list.child_item_completed
end
end
end
class List
attr_accessor :name, :description, :lists, :items
state_machine :list_state, :initial => :available do
after_transition :started=> :completed, :do => :list_completed
event :start do
transition [:available] => :started
end
event :finish do
transition [:started] => :completed
end
state :started do
# take any actions related to moving to started state.
end
state :completed do
# take any actions related to moving to completed state.
end
end
def initialize(list_name, list_description, parent = nil)
@name = list_name
@description = list_description
@lists = []
@items = []
@parent = parent
super() # NOTE: This *must* be called, otherwise states won't get initialized
end
def add_list(name, description)
@lists << List.new(name, description, self)
@lists.last
end
def add_item (name, description)
@items << Item.new(name, description, self)
@items.last
end
# Called by child item when item is completed. Allows check for
# all items completed.
def child_item_completed
# once the first item is completed consider this list started
if self.list_state == "available"
self.fire_events(:start)
end
# if all items are completed move this list to completed state.
if self.items.select{|i| i.item_state != "completed"}.empty?
self.fire_events(:finish)
end
end
def child_list_completed
# if this is a sublist let parent know this list is completed.
# else this is top level list and everything is completed.
if self.items.select{|i| i.item_state != "completed"}.empty?
if self.lists.select{|l| l.list_state != "completed"}.empty?
self.fire_events(:finish)
end
end
end
def list_completed
if @parent
puts "congrats on completing list #{self.name}"
@parent.child_list_completed
else
puts "congrats on completing project #{self.name}"
end
end
end

As you can see there is a rich environment for embedding behavior, including adding it in the state definitions, adding it using the before_transition method, and adding it using the after_transition method. You can also define methods inside the state definition so you can extend the functionality of a state.

Now if you load this example in IRB you can play with the lists and items like so:

 load "list_example.rb"
 list = List.new("Groceries", "Get some groceries")
 wf = list.add_list("Whole Foods", "Get some groceries from Whole Foods")
 milk = list.add_item("Milk", "2% Milk")
 strawberries = wf.add_item("Strawberries", "Ripe organic Strawberries"))
 oranges = wf.add_item("Oranges", "Ripe mandarine oranges")

 strawberries.finish 
 =&gt; finished item Strawberries

 oranges.finish  
 =&gt; finished item Oranges
 =&gt; congrats on completing list Whole Foods

 milk.finish
 =&gt; finished item Milk
 =&gt; congrats on completing project Groceries

This was a really simple example, but shows how easy it is to create classes that embed state machines. For more check out the state_machine gem.

State Diagrams

The library also allows you to generate graphs of the states and transitions in each class. For example:

create a Rakefile.rb

 require 'tasks/state_machine'
 require './list_example.rb

Then in terminal execute:

 rake state_machine:draw CLASS=List 

For the List class shown above this will generate a png file named List_list_state.png that looks like the image below.

The List class is very simple so the diagram is also simple. In the case of my project there are many states and more complicated transitions between states so the diagram can be very handy to visualize what is happening when debugging a strange transition,

Wrapup

This todo list example is very simple but it allows exploring the basic features of the state_machine gem, and demonstrates how simple it is to add state machine functionality to classes. If a class you are designing has variables that keep state, and you are triggering behaviors when those variables change, then a state machine will likely be a more clean approach to organizing behavior in your class.

Comments