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.
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
to in its
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
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
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
statuscolumn in our
Book::Objectclass, with a
- Give the
statecolumn an initial default value of
- Add an instance method called
- Add another instance method called
submit, which changes the object’s
- Add yet another instance method called
submitted?, with (at the very least), a single-line
- Add some more instance methods for good measure, all with some logic in them to keep track of our object’s
- Repeat steps 3-6 for every single new
statewe 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
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
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
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
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
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!
- State machines can be broken down into
events. Events control the flow of one
to transition. Only if a transition occurs successfully will an object’s
statechange. 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!