State Machines in Ruby

To 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.

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.

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.

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:

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:

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.

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 oftransition.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.

Practical Usage

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.

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.

Dave is a web application developer residing in sunny Glasgow, Scotland. He works daily with Ruby but has been known to wear PHP and C++ hats. In his spare time he snowboards on plastic slopes, only reads geek books and listens to music that is certainly not suitable for his age.