Today you’ll learn about state machines, how they work & how to use them in your Ruby projects with the AASM gem.
Now:
Imagine a traffic light…
It can be red, green or yellow.
When it changes colors, the next color is based on the current one.
Let’s say that this is the kind that makes a sound for blind people so they know when they can cross.
Now:
You’re writing the software for this thing.
How are you going to know what sound to play every time & what color should be next?
You could write some if statements like this:
if @light.state == "green" @light.play_green_sound end if @light.state == "green" @light.change_to_yellow end # ...
This state checking code would be all over the place!
How can we improve this?
If you apply Object-Oriented Design principles you’ll re-discover the State Design Pattern.
What is The State Design Pattern?
The State Design Pattern is one way to implement a state machine.
You’ll need 3 components:
- A
Context
class, this class knows what the current state is - A
State
class, this class defines the methods that should be implemented by the individual states - One class for every state. These classes will inherit from the
State
class
In our traffic light example, the context is the TrafficLight
itself.
And the states are Green
, Red
& Yellow
.
Every state will know what to do.
The big benefit?
Every state knows itself so there is no need to check for the current state. This translates into less conditional statements which are often a source of complexity.
Traffic Light Implementation
Let’s see the code for an actual implementation of this pattern.
Here’s the TrafficLight
:
class TrafficLight def initialize @state = nil end def next_state(klass = Green) @state = klass.new(self) @state.beep @state.start_timer end end
Here’s the base State
:
class State def initialize(light) @light = light end def beep end def next_state end def start_timer end end
Yes, these 3 methods are empty.
It’s a common convention in other programming languages (like Java) to define this “interface”, but it’s not popular in Ruby.
This is here for demonstration purposes.
However, we still want to share the initialize
method between all the states because all of them need the context (TrafficLight
object) to signal a state change.
Now:
The 3 states look very similar to each other, so I’m going to show you the code for just one of them.
Here’s the Green
state:
class Green < State def beep puts "Color is now green" end def next_state @light.next_state(Yellow) end def start_timer sleep 5; next_state end end
Every state knows how & when to switch to the next.
AI Game Example
You can use a state machine to solve games that depend on the current state, like RubyWarrior.
In RubyWarrior you’re given a player object & a board.
The goals are to:
- Defeat all the enemies on the board
- Reach the exit while keeping your HP above 0
You can make one move at a time & you have to make a good choice if you want to complete the level.
Looking at the current state helps you make that choice.
That’s why a state machine is a good solution.
Here’s an example:
class Attacking < State def play(warrior) warrior.attack! @player.set_state(Healing) unless enemy_found?(warrior) end end
This is one of the states that our warrior can be in, when we don’t have any enemies in sight we move into the Healing
state to recover from battle damage.
Using The AASM Gem
If you want to keep track of the current state while making sure that the transitions are valid then you can use a state machine gem like AASM.
This gem is built around the idea of events (like pressing a light switch) that trigger transitions into other states.
Here’s an example:
require 'aasm' class Light include AASM aasm do state :on, :off event :switch do transitions :from => :on, :to => :off, :if => :on? transitions :from => :off, :to => :on, :if => :off? end end end
How to use this class:
light = Light.new p light.on? # true light.switch p light.on? # false
Using this state machine you can only transition to the “on” state if the current state is “off”. You can also have a number of callbacks (before/after) to run specific code during state transitions.
These callbacks could include things like:
- Sending an email.
- Logging the state change.
- Updating a live monitoring dashboard.
In addition, AASM has the option to save the current state to a database using ActiveRecord
.
AASM Gem Video
Summary
You have learned about state machines, the state design pattern & the AASM gem! Keep learning now by subscribing to my Ruby newsletter (15000+ subscribers) so you don’t miss new articles & subscriber-exclusive Ruby tips.
Now it’s time to practice with these new ideas 🙂
Thanks for reading!
Regarding to AASM gem am using in projects those i working on enum or enumerize
so i can check the states of the object and get it based on locale
Excellent and informative! Thanks for helping me to rediscover the State Pattern.
Thanks for reading! 🙂
Nice article, thanks! About traffic light implementation, I see you pass TrafficLight instance into State and also create State instance in TrafficLight, I think it’s circular dependency, any idea?
I don’t think it should be a problem because the state is not accessing itself through the
TrafficLight
. In fact,TrafficLight
has no attribute accessors, so there is no way to accessstate
(unless you use metaprogramming).Thanks for your comment! 🙂