Originally posted on my personal blog.
I discovered State Machines about 2 years ago, it was used in a solution to a problem where we were mapping the possible states of a VoIP phone call (incoming, ringing, answered, etc.) to something that we could monitor. I was amazed at how simple this was and decided to adopt state machines in my own projects.
I’m sure everyone knows what happens when you discover some new software principle; you decide that literally everything you’ve ever built needs it. However, as responsible developers, we must ask ourselves whether what we're trying to do is the best solution. We must ask what our use case is and, in this case, whether we even need a state machine. Perhaps the example below will help provide insight into what state machines can be used for.
STATE MACHINES
Before we get into implementing a simple state machine in Python lets quickly go over what a state machine is and what it looks like. In short, it’s basically a set of states and a set of actions/events, we start at one state and like a graph data structure, we can traverse through the nodes based on the condition described by the corresponding edge. We use that condition to get to the node (aka state) we’d like to. Since there is only a single state active at a time, we have a lot of control in terms of where we are within the lifecycle of the state machine. There’s a more thorough CS theory backed explanation that can be found by means of a video here. I highly recommend checking it out if you’d like to know more!
USE CASES
There are many use cases for state machines, some of which include — managing states (like call states, WiFi connectivity, and even the Android activity life cycle) or reporting metrics — duration of time for a user to complete a login (login -> pending -> success) for example.
State machines are especially interesting because, among other things, they provide well-defined scenarios and list out the conditions to get to them. This makes it very easy to scope out edge cases and how to handle them, as we are forced to consider every possible scenario our code must fall within.
Personally, the best way to understand state machines is through an everyday example.
Imagine you are looking at your password protected phone, at a high-level it has two states of operation. The first being locked, where you have limited functionality and the second being unlocked, where you can now use the device in a greater capacity.
This is what the above state machine looks like when visualized.
USING A STATE MACHINE
We begin by defining the states, these are defined as the nodes within the state machine. In our case, we have two states; locked & unlocked. In the example below, I've also defined a State object which will handle some utility functions for our states (which extend from this object).
# state.py
class State(object):
"""
We define a state object which provides some utility functions for the
individual states within the state machine.
"""
def __init__(self):
print 'Processing current state:', str(self)
def on_event(self, event):
"""
Handle events that are delegated to this State.
"""
pass
def __repr__(self):
"""
Leverages the __str__ method to describe the State.
"""
return self.__str__()
def __str__(self):
"""
Returns the name of the State.
"""
return self.__class__.__name__
The states can then be defined as follows.
# my_states.py
from state import State
# Start of our states
class LockedState(State):
"""
The state which indicates that there are limited device capabilities.
"""
def on_event(self, event):
if event == 'pin_entered':
return UnlockedState()
return self
class UnlockedState(State):
"""
The state which indicates that there are no limitations on device
capabilities.
"""
def on_event(self, event):
if event == 'device_locked':
return LockedState()
return self
# End of our states.
Then we define the actual state machine. It's fairly simple and looks like this:
# simple_device.py
from my_states import LockedState
class SimpleDevice(object):
"""
A simple state machine that mimics the functionality of a device from a
high level.
"""
def __init__(self):
""" Initialize the components. """
# Start with a default state.
self.state = LockedState()
def on_event(self, event):
"""
This is the bread and butter of the state machine. Incoming events are
delegated to the given states which then handle the event. The result is
then assigned as the new state.
"""
# The next state will be the result of the on_event function.
self.state = self.state.on_event(event)
Fairly simple right? What this state machine does is defines a starting state LockedState and exposes a function to handle events. This function basically assigns the current state to the result of that same state when it handles the event.
Finally, we can test that implementation of the state machine using the python shell.
$ python
Python 2.7.13 (default, Apr 4 2017, 08:47:57)
[GCC 4.2.1 Compatible Apple LLVM 8.1.0 (clang-802.0.38)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>
>>> from simple_device import SimpleDevice
>>> device = SimpleDevice()
Processing current state: LockedState
>>>
>>> device.on_event('device_locked')
>>> device.on_event('pin_entered')
Processing current state: UnlockedState
>>>
>>> device.state
UnlockedState
>>>
>>> device.on_event('device_locked')
Processing current state: LockedState
>>>
>>> device.state
LockedState
>>> device.on_event('device_locked')
You’ll notice that duplicate events are ignored and only the events that provide transitions are made use of. This becomes a very powerful tool when we want to ignore events or reduce a series of verbose events into a simpler set of events. My favorite thing is that if in the future we'd like to add more states and transitions, it's very simple to do so without rewriting a huge chunk of our codebase.
CONCLUSION
State machines are awesome, from cases that require simple state management, to metric reporting, they have proven to be very useful and extensible. The above technique was a product of implementing a state machine to handle SIP signaling events (for VoIP) and measure deltas between incoming events (to gain a better understanding of our pain points). It can definitely scale to a few dozen states and makes for a simple and easy state measurement.
If you are looking for a different solution, check out the Python transitions library which is a state machine library that looks fairly promising.
REFERENCES
While researching state machine libraries for Python I came across a page that documented a simple implementation, the above solution based on the example provided. Check out the original implementation here.
Let me know if you spot any errors, or if you just want to say hi!