State Machines in Ruby

Tweet

Virtual_finite_state_machine_executor_flow_chartTo give them their Sunday name, Finite State Machines (FSMs) are all around us and if we open our eyes long enough you can see them in play when you buy a can of soda, progress through e-commerce shopping carts, pass through electronic turn styles in train stations. Back in my previous life as an Electronic Engineer we would implement FSMs all the time using a spaghetti network of logic gates. And it’s true in the software world we will spaghetti our code with if statements that really should be cleaned up using the State Pattern.

Dissection of a Finite State Machine

There is really nothing to complex when explaining a FSM. The clue really is in the name, we have a system, module, class, machine etc. with a known number of states. In software terms when they become important is when some internal state matters for the behavior of an object. In essence there are three core concepts for FSM, States (duh), Transitions and Events.

As an example lets look at a really simple soda machine. The trick as I see it is to identify the correct object as an FSM. At first glance you may be tempted to pick states such as idle, coins inserted, drink dispensed back to idle. This isn’t a good state machine, although it describes the overall behavior what we have done is mix the behavior of a soda machine and soda machine transaction into a single object.

Implementing a State Machine

The best place to start with a known problem in the Ruby world is of course RubyGems. A quick look on Ruby Toolbox and we get a smorgasboard of state machine gems. My personal preference is the aptly named State Machine gem.

My reason for choosing this over anything else is a mix of history (previous experience) and its wealth of integrations. Although not tied directly to Rails, it does have ActiveRecord and ActiveModel integration. These integrations mean we can use nice features such as observers, validation and in the ActiveRecord world we get named scopes and database transactions on transitions. It also plays nicely with other database layers such as DataMapper, Sequel and Mongoid.

For now we will simply use the state_machine gems ability to add FSM behavior to plain old Ruby objects.

If you would imagine the machine will create a new instance of SodaTransaction whose initial state is awaiting_selection. The transition path will follow something along the lines of: awaiting_selection, dispense_soda, complete.

require 'rubygems'
require 'state_machine'

class SodaTransaction

  state_machine :state, initial: :awaiting_selection do
  end

end

We know instinctively the awaiting_selection state must react to a button press on the front panel. So lets go ahead and implement that.

require 'rubygems'
require 'state_machine'

class SodaTransaction

  state_machine :state, initial: :awaiting_selection do

    event :button_press do
      transition :awaiting_selection => :dispense_soda
    end

  end

end

At this point we can try out our soda transaction in irb. A quick tip I have found is to run the command irb -I . from the path our soda_transaction.rb file resides. This simply starts an irb session with the working directory set at the current path allowing you to simply use require 'soda_transaction' with no other path necessary.

require 'soda_transaction'

sm = SodaTransaction.new
puts sm.state

#=> awaiting_selection

sm.button_press
puts sm.state
#=> dispense_soda

Huzza! We achieve the behavior we want and getting closer to quenching our thirst to boot. One thing that really annoys me about soda machines is when my favorite tipple is out of stock. The machine waits for me to insert my coins and press the button before telling me I’m going to have to settle for Dr. Pepper. I would hate to deprive anyone of the same rage so lets implement that behavior as well.

class SodaTransaction

  attr_accessor :selection

  state_machine :state, initial: :awaiting_selection do

  event :button_press do
    transition :awaiting_selection => :dispense_soda, if: :in_stock?
  end

  end

  def in_stock?
    stock_levels[@selection] > 0
  end

  def stock_levels
    {
      dr_pepper: 100,
      sprite: 10,
      root_beer: 0,
      cola: 8
    }
  end

end

What we have done here is simply placed a guard on the transition for :awaiting_selection to :dispense_soda. By using a simple look up on a hash we check the desired drink is in stock.

Obviously, in a real implementation we would want to handle decrementing the stock and delegating this kind of check elsewhere in the application. For now a simple stubbed hash lookup is just fine. But you can see from the example the selection is an instance variable, where is that set? Surely in the event?

The best way I have found to allow these additional parameters to be passed to the event is to override the event its self as so:

class SodaTransaction

  state_machine :state, initial: :awaiting_selection do

    event :button_press do
      transition :awaiting_selection => :dispense_soda, if: :in_stock?
    end

  end

  def button_press(selection)
    @selection = selection
    super
  end

  def in_stock?
    stock_levels[@selection] > 0
  end

  def stock_levels
    {
      dr_pepper: 100,
      sprite: 10,
      root_beer: 0,
      cola: 8
    }
  end

end

Because of the way the methods are declared in state machine we can once we have completed any custom actions in or new method we simply call super to pass the call back up the tree to the original event.

On With the Soda

After that slight detour into the bowels of user experience we still need to complete the transaction for the soda. For this we need another event that will fire when the soda can drops from it’s tray. Quite simply we setup this event moving from dispense_soda state to `complete:

class SodaTransaction

  state_machine :state, initial: :awaiting_selection do

    event :button_press do
      transition :awaiting_selection => :dispense_soda, if: :in_stock?
    end

    event :soda_dropped do
      transition :dispense_soda => :complete
    end

  end

  def button_press(selection)
    @selection = selection
    super
  end

  def in_stock?
    stock_levels[@selection] > 0
  end

  def stock_levels
    {
      dr_pepper: 100,
      sprite: 10,
      root_beer: 0,
      cola: 8
    }
  end

end

Running a couple of lines in irb shows that we have successfully dropped a soda for our user to enjoy.

require 'soda_state_machine'

sm = SodaTransaction.new
puts sm.state

#=> awaiting_selection

sm.button_press(:cola)
puts sm.state

#=> dispense_soda

sm.soda_dropped
puts sm.state

#=> complete

This leaves just one thing is bugging us now and thats managing the stock of our soda machine. From the above example we are now one can of cola less.

Observers and Callbacks

Fortunately managing this would be trivial using the state_machine gem. It provides a number of transition callbacks that work similarly to Rails filters.

We could set a callback on the :soda_dropped event like so.

class SodaTransaction

  state_machine :state, initial: :awaiting_selection do

    after_transition :on => :soda_dropped, :do => :manage_stock

    event :button_press do
      transition :awaiting_selection => :dispense_soda, if: :in_stock?
    end

    event :soda_dropped do
      transition :dispense_soda => :complete
    end

  end

  def button_press(selection)
    @selection = selection
    super
  end

  def manage_stock
    puts "Removing 1 from the #{@selection} count"
  end

  def in_stock?
    stock_levels[@selection] > 0
  end

  def stock_levels
    {
      dr_pepper: 100,
      sprite: 10,
      root_beer: 0,
      cola: 8
    }
  end

end

As I’m using a simple hash for stock management I’m just printing out that SodaTransaction object is sending out some message to remove 1 from the current stock count.

It doesn’t feel quite right though from a design point of view SodaTransaction is now managing the stock of the soda machine. My gut tells me we may want to look at handling this another way. Don’t get me wrong in the simple machine we have implemented what we gave is fine in my opinion.

In the case where we need more, such as logging times when more drinks are sold, or recording exits from the purchase funnel using these callbacks with Observers may be more applicable.

class SodaStockObserver

  def self.manage_stock(transaction, transition)
    puts "Removing 1 from the #{transaction.selection} count"
  end

  def self.after_transition(transaction, transition)
    puts "#{transition.attribute} was: #{transition.from}, #{transition.attribute} is: #{transition.to}"
  end

end

class SodaTransaction

  attr_reader :selection

  state_machine :state, initial: :awaiting_selection do

    after_transition :on => :soda_dropped, :do => SodaStockObserver.method(:manage_stock)
    after_transition SodaStockObserver.method(:after_transition)

    event :button_press do
      transition :awaiting_selection => :dispense_soda, if: :in_stock?
    end

    event :soda_dropped do
      transition :dispense_soda => :complete
    end

  end

  def button_press(selection)
    @selection = selection
    super
  end

  def in_stock?
    stock_levels[@selection] > 0
  end

  def stock_levels
    {
      dr_pepper: 100,
      sprite: 10,
      root_beer: 0,
      cola: 8
    }
  end

end

As you can see using an observer on the transition callbacks can be really powerful for not much effort. A note about the observer code is the use of
transition.attribute in the after_transition method. This simply represents what key we have given our state machine, in this case “state”.

As mentioned earlier the state_machine gem has some lovely integrations with popular database layers such as ActiveRecord. Part of these integrations is ‘automagic’ callbacks. For example if the SodaTransaction object inherited from ActiveRecord then simply defining a SodaTransactionObserver would take care of wiring up for us.

class SodaTransaction < ActiveRecord::Base
  ...
end

class SodaTransactionObserver < ActiveRecord::Observer

  def after_soda_dropped(transaction, transition)
    # Remove 1 from selection qty.
  end

  def after_transition(transaction, transition)
    # Do whatever logging we need
  end

end

Practical Usage

As far as state machines in the wild go there are plenty out there. Projects which spring to mind are Spree, RestfulAuthentication and of a non Ruby flavor EmberJS.

Spree uses a FSM in the checkout process, moving from taking various payment details, to order completion. You can even hook into the FSM and inject your own states to further customize the checkout process.

RestfulAuthentication (does anyone use this since Devise?) implements a state machine (acts_as_state_machine). It manages the account for a user, ‘pending’, ‘active’, ‘suspended’ etc.

Ember on the other hand, although obviously not written in Ruby uses a state machine as the principle of it’s routing. Where as the user navigates through the site the router uses the URI to decide upon the applications state.

There was also a point in time where a state machine was integrated into ActiveRecord for a while, but was removed pre 3.0 release.

In my experience they are a real ace in the hole for developers and I was surprised about how little they are actually used. That said, FSMs can be implemented in the wrong situations making life miserable in general. I guess the take away advice would be, by all means start out just using an array of states and a state instance variable on your objects. But never be afraid to implement a fuller FSM solution when the former becomes too unwieldy.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • David

    Nice write up, a fan of FSM myself.

    The code examples are not properly displayed, specifically the operators (less than, greater than).