Words and Code

One writer’s journey from words to code.

A Machine State of Mind, Part 2: Implementing State Machines


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
class Book::Order
  include AASM
end

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
aasm do
  state :unplaced, :initial => true
  state :submitted

  event :submit do
    transitions :from => :unplaced, :to => :submitted
  end
end

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
  aasm do
    state :unplaced, :initial => true
    state :submitted
    state :processing
    state :shipped
    state :completed
    state :returned
    state :deleted

    event :submit do
      transitions :from => :unplaced, :to => :submitted
    end

    event :process do
      transitions :from => [:submitted, :returned], :to => :processing
    end

    event :ship do
      transitions :from => :processing, :to => :shipped
    end

    event :complete do
      transitions :from => :shipped, :to => :completed
    end

    event :return do
      transitions :from => :complete, :to => :returned
    end

    event :delete do
      transitions :from => :processing, :to => :deleted
    end
  end

end

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
order = Book::Order.new # => => #<Book::Order:0x007fad3d51aa30>
order.unplaced?         # => true
order.may_submit?       # => true
order.submit            # calls the `submit` event

order.submitted?        # => true
order.unplaced?         # => false
order.may_submit?       # => false
order.submit            # => raises AASM::InvalidTransition

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:

  1. Make a migration that adds a state or status column in our Book::Object class, with a string value.
  2. Give the state column an initial default value of unplaced.
  3. Add an instance method called unplaced? with a boolean return value.
  4. Add another instance method called submit, which changes the object’s state property from "unplaced" to "submitted".
  5. Add yet another instance method called submitted?, with (at the very least), a single-line if conditional.
  6. Add some more instance methods for good measure, all with some logic in them to keep track of our object’s state.
  7. 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
def order_shipped_email
  # Sends an email informing the User
  # that their Order has been shipped
end

order.ship do
  order.user.send_order_shipped_email
end

# Only if order.may_ship? returns `true`,
# will the `send_order_shipped_email`
# method actually fire.

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
state :shipped, :before_enter => :print_return_label
state :deleted

event :ship do
  transitions :from => :processing, :to => :shipped
end

event :delete do, :after => :send_delete_confirmation do
  transitions :from => :processing, :to => :deleted
  end
end

def print_return_label
  # Prints order details along with
  # the return label information.
end

def send_delete_confirmation
  # Sends a confirmation email
  # that the `Order` has been deleted.
end

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
event :submit do
  transitions :from => :unplaced, :to => :submitted, :guard => :payment_successfully_processed?
end

def payment_successfully_processed?
  # Returns a truthy value based on
  # whether the user's credit card info
  # has been processed successfully or not
end

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 and events. Events control the flow of one state to another.
  • Each event has a from and a to transition. Only if a transition occurs successfully will an object’s state 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!