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.
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.
The State Design Pattern is one way to implement a state machine.
You’ll need 3 components:
Contextclass, this class knows what the current state is
Stateclass, this class defines the methods that should be implemented by the individual states
In our traffic light example, the context is the
And the states are
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.
Let’s see the code for an actual implementation of this pattern.
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
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.
The 3 states look very similar to each other, so I’m going to show you the code for just one of them.
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.
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:
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.
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:
In addition, AASM has the option to save the current state to a database using
You have learned about state machines, the state design pattern & the AASM gem! Keep learning now by subscribing to my Ruby newsletter (7000+ 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.