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 (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!
Related
5 comments
MaJeD BoJaN says
4 years ago
Regarding to AASM gem am using in projects those i working on enum or enumerize
enumerize :status, in: {
pending: 1, # When user login first time will be pending
active: 2, # After user confirm his mobile number will be active
inactive: 3 # Admin can disable users and those uses will not be able to login or use the app
}, default: :pending, scope: true, predicates: true
so i can check the states of the object and get it based on locale
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 access state (unless you use metaprogramming).