Tired of conditionals? State pattern could help
When writing code, our classes often go through a series of transformations. What starts out as a simple class will grow as behavior is added. And if you didn’t take the necessary precautions, your code will become difficult to understand and maintain. Too often, the state of an object is kept by creating multiple boolean attributes and deciding how to behave based on the values. This can become cumbersome and difficult to maintain when the complexity of your class starts to increase.
This is a common problem on most projects, and it is wise to model it with
a Finite State Machine (a.k.a FSM).
In fact, there is a design pattern called State
that address this very well,
so you can find hundreds of gems implementing this pattern.
A great description of the problem can be found on SourceMaking.org:
A monolithic object’s behavior is a function of its state, and it must change its behavior at run-time depending on that state. Or, an application is characterized by large and numerous case statements that vector flow of control based on the state of the application.
What is a Finite State Machine?
Finite state machines are an incredibly useful tool for modeling objects that change their behavior based on the state they happen to be in. As I mentioned, this is a situation that happens in a lot of projects, so if you hadn’t already heard about FSM, I strongly recommend you check out Basics of Automata Theory which is a great starting point.
That being said, here’s a quick informal definition anyway:
A finite state machine is a mathematical abstraction used to design algorithms. The machine is in only one state at a time. It can change from one state to another when initiated by a triggering event or condition; this is called a transition. A particular FSM is defined by a list of its states, and the triggering condition for each transition.
In practice, state machines are often used for:
- Design purposes
- Natural language parsers
- String parsing
- Communication protocols
- Algorithms
- etc
In fact, here is a list of real life examples modeled with FSM:
- Traffic Light
- Vending Machine
- Android’s class: MediaRecorder
- Rails router: journey
- Transmission Control Protocol (TCP)
- AI algorithms: Pac-Man’s ghosts
What does a Finite State Machine look like?
A picture is worth a thousand words, maybe more on this topic.
The Pac-Man’s ghosts FSM caught my attention.
It is so simple to read and understand the ghosts behavior on Pac-Man’s game that anyone who can speak English can understand it, no programming skills required!
The ghosts in Pac-Man have four behaviors:
- Randomly wander the maze
- Chase Pac-Man, when he is within line of sight
- Flee Pac-Man, after Pac-Man has consumed a power pellet
- Return to the central base to regenerate
These four behaviors correspond directly to a four-state FSM. Transitions are dictated by the situation in the game. For instance, a ghost FSM in state 2 (Chase Pac-Man) will transition to state 3 (Flee) when Pac-Man consumes a power pellet.
Designing a State Machine with State pattern
The state pattern is a behavioral object design pattern. The idea is to change an object’s behavior depending on its state. In the state pattern, we have the following classes:
-
Context
class: This class has a state reference to aConcrete State
instance. -
State
base class: Declares particular methods that represent the behaviors of a particular state. -
Concrete States
class: Implement the behaviors of a particular state.
By changing a Context
’s Concrete State
, we change its behavior. This is an
elegant solution because it encapsulates the set of behaviors that are specific
to a certain state.
Some benefits using this design pattern are:
- Avoiding inconsistent states
- Putting all associated behavior together in one state object
- Removes monolithic
if
orcase
statements
Summarizing: The state design pattern allows for full encapsulation of an unlimited number of states on a context for easy maintenance and flexibility.
Because this is a common, repeatable problem that engineers find themselves encountering I have put together a step-by-step process for architecting your own finite state machine. There are four simple steps:
- Identify the domain model
- Enumerate the valid states
- Describe the state-related events
- Specify transitions
1. Identify the domain model
Let’s go through an example and say that we want to model a Lamp
.
A Lamp
have many attributes, like brand, price, voltage, watts, etc
but for this example I want to use the state of a Lamp
, the simple on/off
I don’t care about other possible states like broken for this example.
It is very important to identify the real object that owns a state machine or
later you will find your code hard to maintain and understand and may end up
refactoring.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Lamp
attr_accessor :state, :brand, :voltage
def turn_on
fail 'It is already on' if state == :on
state = :on
end
def turn_off
fail 'It is already off' if state == :off
state = :off
end
end
2. Enumerate the valid states
Keep in mind that the purpose of State
pattern is to “represent the state
of an object”. In our Lamp
example, we would create two concrete state classes:
OnLampState
, OffLampState
.
State classes will only model valid states of the lamp, removing the problem when there is one combination of values that does not correspond to a valid state for your lamp (generally, when the object uses multiple booleans to represent a state).
In the Lamp
example, those states are
:on
:off
So make a list with all the states you want to identify, or expect different behavior. As simple as that.
3. Describe the events which change or involve the state
Every model with a state
has a method where it changes a value, or behaves
differently depending on the state
. Those methods are potential events.
In our example, I evaluated these to be:
turn_on
turn_off
In this step, you have to list the events that will trigger a state transition.
4. Specify the transitions between states
Finally, we have to write down every single branch of the state machine. When I say branch I mean the process to fire an event from a state A and move to the state B.
The transitions are composed by two states: from
and to
, and there
are triggered when an event is fired.
The Lamp
example shows us only two possbile transitions:
- from
:off
to:on
- from
:on
to:off
List every possible ending state from each valid state. At this point you should have three lists:
1. Lamp States
:on
:off
2. State Events
turn_on
turn_of
3. Transitions
- from
:off
to:on
- from
:on
to:off
Ruby implementation using aquam
You already been through the hardest part: you were thinking about the states, every possible transition, you argued with your teammates, you did some sketches (if you didn’t, you should try) until you reached the solution. Your Finite State Machine is ready, now you only have to code it!
You probably are anxious to test your FSM, so I will be short.
Introducing aquam
aquam
simplifies the design by introducing the various parts of a
real state machine, including states, events and it meets our criteria
for this problem
- Full object oriented
- Framework-agnostic
- No dependencies
- Allows the developer to make important design decisions
I’m gonna use this gem that we developed for a past project because it suits perfectly with our criteria.
Write your FSM guidelines
Taking the three lists you build in the design step, use aquam DSL to write it down. It will look like
1
2
3
4
5
6
7
8
9
10
11
12
class LampStateMachine < Aquam::Machine
state :on, OnLampState
state :off, OffLampState
event :turn_on do
transition from: :off, to: :on
end
event :turn_off do
transition from: :on, to: :off
end
end
Represent the different states
The State pattern does not specify where the state transitions will be defined.
There are two choices: the context object, or each individual State class. The advantage of the latter option is ease of adding new State classes. The disadvantage is each State class has knowledge of its siblings, which introduces dependencies between them.
Every bit of behavior that is state-dependent should become a method in the
concrete state classes. Also, aquam
transforms every event defined into a
method in the concrete state class.
Example Option 1 - Context Object Class
1
2
3
4
5
6
7
8
9
10
11
12
13
class Lamp
def turn_off
current_state.turn_off
self.state = :off
end
def turn_on
current_state.turn_on
self.state = :on
end
end
Example Option 2 - State Classes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class OnLampState < Aquam::State
use_machine LampStateMachine
def turn_off
# Do something
@object.state = :off
end
end
class OffLampState < Aquam::State
use_machine LampStateMachine
def turn_on
# Do something
@object.state = :on
end
end
Delegate to the state
In order to allow an object to alter its behavior when its internal state changes we need to delegate the events to the current state class. The object will appear to change its class.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Lamp
attr_accessor :state, :brand, :voltage
def turn_on
state_machine.current_state.turn_on
end
def turn_off
state_machine.current_state.turn_off
end
def state_machine
@state_machine ||= LampStateMachine.new self
end
def state
@state ||= :off
end
end
NOTE: Remember to always set the initial state
Play with your Lamp
And that’s it! You can play with your lamp, using a real state machine. Try to turn it on when it is already on, add a few more states and see how it changes it behavior!
Good reading
If you enjoyed this article, you should know that Finite State Machines are studied in the more general field of Automata theory, a theoretical branch of computer science.
Here are some links that you might want to read:
- Basics of Automata Theory by Standford University
- Applications of Deterministic Finite Automata by Eric Gribkoff
- A router for Rails by Aaron Patterson at RubyConfArgentina 2011