This blog post is part of a series on State Machines. Read Part 1 here.
Until you encounter a state machine in a gem, framework, or within someone else’s code, you probably won’t find one very easily. But as we learned last week, they’re rather pervasive. I discovered state machines while helping build a large-scale eCommerce website.
But there actually weren’t even that many state machines in our code! We were relying on state machines that lived in the source code of a Rails library with a variety of gems, commonly referred to as spree. (Why reinvent ecommerce platforms when so many people have already made ‘em, amirite?)
So, I did what any self-respecting, completely unaware new developer would do: I dove into the spree source code. And boy, was that a rabbit hole. But, I learned some things about how state machines work in Rails and how to make them. It’s good to conceptually understand the theory behind state machines, but the best way to learn something is by doing it. It’s time to take off the training wheels and actually build our own state machine!
Starting Up The Machine Engine
There are a few different options for implementing state machines in a Rails application; spree, for example, uses the state_machine plugin. Personally, I prefer the acts_as_state_machine gem (aasm), as I’ve found it to be a bit easier to use and understand.
Once we gem install aasm
and add it to our application’s Gemfile
, we’ll want to include it in the body of the class we’re trying to implement the state machine on. In the case of our bookstore application, our Order
objects are what will be transitioning from one state to another. As the application begins to grow, it’ll be useful to namespace our objects (Book::Order
) before including the module:
1 2 3 |
|
Including the gem is the easy part. The next part is slightly trickier, yet remains pretty intuitive. First, we’ll start by defining two states: an initial state
that we want our object to start off in, and a second state
we want our object to transition to. Then, we’ll want to adding an event
with an from
and to
in its transition
:
1 2 3 4 5 6 7 8 |
|
Notice that the event :submit
is the present tense verb form of the state :submitted
. And when the submit
event is called, the Book::Order
object’s state
will transition from one state to the other.
That’s pretty much all you really need to implement a state machine. But of course, we’ve only got two states here, which isn’t really much of a machine and definitely doesn’t take advantage of all the functionalities that aasm
provides us with.
Let’s continue to build on the state machine based on the diagram from last week’s post and add a few more states and events:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
|
Whoa – now we’re talking! This state machine is even more complex than the diagram we started off with!
You’ll notice that we even have a state :returned
and an event :return
, which transition from a complete
state to a returned
one. And if you’ve got a really good eye, you’ll see that the process
event has changed, too. Now, we can call the process
event on the object when it’s either in the submitted
state or the returned
state.
Our state machine is now a self-referential structure, which means that the process
event looks back to states within the machine to determine whether it can continue forward or not. This all seems pretty cool, but you better buckle in – it’s about to get even cooler.
Test Driving Your Machine
It’s lovely that we have this machine and all, but what’s the fun if you can’t take it out for a whirl? So, let’s see what this thing can do.
This gem in particular provides us with a variety of public methods for any instances of our Book::Order
class, all via our state machine:
1 2 3 4 5 6 7 8 9 |
|
Protip: If you’re not a big fan of raising exceptions in your application, just add aasm :whiny_transitions => false do
right inside of your class, and you’ll return basic boolean
values instead of exceptions.
Whew! So that’s a lot of methods. But we didn’t have to write any of them! Isn’t that fantastic? Hopefully the usefulness of state machines is starting to come together now. Remember before we knew what a state machine was? How would we have had to handle all of this functionality?
We would’ve had to do all of the following, multiple times:
- Make a migration that adds a
state
orstatus
column in ourBook::Object
class, with astring
value. - Give the
state
column an initial default value ofunplaced
. - Add an instance method called
unplaced?
with aboolean
return value. - Add another instance method called
submit
, which changes the object’sstate
property from"unplaced"
to"submitted"
. - Add yet another instance method called
submitted?
, with (at the very least), a single-lineif
conditional. - Add some more instance methods for good measure, all with some logic in them to keep track of our object’s
state
. - Repeat steps 3-6 for every single new
state
we wanted to add.
If we compare this horrifying list to our state machine, it’s pretty clear that our machine takes care of all of this! Yes, it requires a little bit of setup when we create our class, but come on, it’s like, four lines of code for each state! So much better. And you get all these methods for free! And they’re pretty powerful, because we can call them anywhere in our code, on any instance of our object.
But can we customize this machine even further? Heck yes, we can!
Trick Out Yo’ Ride
Now that we’ve created a basic state machine, we can trick it out with any (or all!) of these options:
1. Pass a block to an event
Whenever you want a specific event to call a particular method, simply pass a block to the method. The block will only be called if the transition occurs successfully.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
2. Use a callback
The aasm
documentation defines a list of different callbacks you can use for your transitions; the callbacks will only be triggered when certain conditions are met (for example, when you exit a particular state
).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
These callbacks work exactly as you might think, hooking into either a state or an event. Before the Order
switches states from processed
to shipped
, the print_return_label
method will fire. But the send_delete_confirmation
method will only be called after the delete event
is finished – only after the transition from processing
to deleted
occurs successfully.
3. Implement a guard
If you want to only allow a transition if a particular condition is defined; if the guard returns false
, the transition will be denied, and will either return false
or raise an error.
1 2 3 4 5 6 7 8 9 |
|
These are just three things you can do to spice up your state machine. Creating a state machine with this gem gives you a fair amount of flexibility. You can use multiple guards or build multiple transitions for a single event. As your state machine grows, you can call the aasm.current_event
to keep track of where you are in your code.
Now that you know how to implement a state machine, hopefully you now realize the value in them and don’t feel too intimidated. As long as you take it a step at a time, you can create your own state machine, with the exact kind of functionality your program needs. With that said, there’s only one thing left to do: go forth implement one yourself! Fly young grasshopper, fly!
tl;dr?
- State machines can be broken down into
states
andevents
. Events control the flow of onestate
to another. - Each
event
has afrom
and ato transition
. Only if a transition occurs successfully will an object’sstate
change. You can manipulate how an event or transition works using callbacks, blocks, and guards. - Find more great blog posts that implement FSM’s here and here. And if you want to get really fancy, learn how to use this!