As an application developer for Mojo Lingo, I’ve had the opportunity to work on many telephony applications in various stages: from an idealistic idea to complete mess of code. One of the most useful patterns I find myself using over and over again is the state machine.
What Are State Machines?
State machines, or more specifically finite-state machines in computer science terms, encapsulate the idea that an object:
- Can be in one of a number of states at any given time
- Can transition between states by a trigger
- Has limitations on which transitions are allowable among states
A simple example of a state machine would be a traffic light. At any given time a traffic light can be in one state: red, yellow, or green. It can also only transition to certain states:
- red -> green
- green -> yellow
- yellow -> red
A standard traffic light cannot go from green to red, bypassing yellow, or yellow back down to green.
The behaviour of the object is enforced by the state and its transitions between states.
Why Should You Use Them?
State Machines allow us to describe the behaviour of an object in a simpler and finite way. Transitions, and their triggers or events, determine allowable states of any given instance.
There are several excellent gems for adding state machines to ruby projects, if you’re looking for a good one to start with, try the state_machine gem.
Imagine we are modelling a phone call for our telephony application. A call is either answered or not, so we have a boolean
answered in our model.
class PhoneCall attr_accessor :answered def initialize self.answered = false end end
Now our application grows, we want to report if the call failed or just wasn’t answered. So we add a boolean
failed and maybe a
class PhoneCall attr_accessor :answered, :failed, :failure_reason def initialize self.answered = false self.failed = false end end
This is kinda ugly: a call be both
failed at the same time.
Ok, so we implement methods to enforce these rules:
class PhoneCall attr_reader :answered, :failed, :failure_reason def initialize @answered = false @failed = false end def answer @answered = true end def fail(reason) @failed = true @failure_reason = reason end end
That’s better, but a successfully
answered call exposes the method
failure_reason, which doesn’t make sense in the context of an answered call. Without some meta-programming there’s not much we can do, so we carry on.
Now we also want to track if the call is dialing or in progress, and the length of the call. So we start adding more methods, add more checks to each method for the various booleans, implement some custom validators to ensure a call cannot be
Our model is starting to get complex, and as we add more states and attributes, it gets exponetially more complex.
Replacing with State Machines
It’s obvious to us that a phone call can only be in one state at a time:
And that only certain states can go to other states:
In addition, only certain things make sense at certain states:
failure_reasononly makes sense if the call failed.
durationonly makes sense for completed calls.
All of this behaviour can be easily described using a state machine instead of manually managing multiple booleans and flags:
# Using the state_machine gem. class PhoneCall state_machine :state, :initial => :dialing do event :answered do transition :dialing => :in_progress end event :failure do transition :dialing => :failed end event :hangup do transition :in_progress => :completed end state :completed do def duration @end_time - @start_time end end state :failed do attr_accessor :failure_reason end before_transition :dialing => :in_progress, :do => :rec_start_time after_transition :in_progress => :completed, :do => :rec_end_time end def answered? in_progress? || completed? end private def rec_start_time @start_time = Time.now end def rec_end_time @end_time = Time.now end end
While the class may have more lines, it is much more obvious what is happening. The state machine enforces a lot of the business logic we were trying to do manually with booleans, flags, and validators. By introducing the concept of states and events, the class’s API is much more semantic. Extending functionality or adding more states is as simple as defining a new state or event. Testing your object’s behaviour becomes simpler; be sure to unit test that your transition paths are enforced and any events are correctly invoked.
When Should You Use State Machines?
I have a few general things I consider for when to use state machines:
- More than one boolean field in your class.
- You have a
status(or similar) attribute.
- You allow the deletion of a instance (don’t delete it, make a
- Your object has a defined process it follows.
- If you’re thinking about adding one.
In addition, state machines are usually quite easy to retrofit into a class that can benefit them. Don’t be afraid to refactor one in!
State machines are a great tool at your disposal for simplifying the behaviour of classes. They might seem like a lot of work at first, but as your system and objects grow, they allow you to reduce complexity dramatically. You can concentrate on functionality of the object, rather than enforcing the rules of it’s behaviour.