Skip to main content

41 The State_Machine Gem

Episode 85 · August 20, 2015

Learn how to use the state_machine gem to keep track of objects in your Rails apps

Models ActiveRecord


Transcripts

This episode we're going to talk about using state machines in your rails application, and how to install them and use them. If you're not familiar with what a state machine is, take a look at the episode I've linked to in the notes, and above in the YouTube video, where I basically just explain what a state machine is. This is something really to keep track of the state of an object in your system. This is something that is really really useful in software, and a lot of places are going to be using this, especially stuff sort of like background workers, they're going to need to keep track of the state, and transition things around, and this is really crucial, especially when you're doing something complex like you have a vehicle, so if you have a vehicle, you want to be able to define the state that initially it's parked, but when you go from parked to anything, you want to put on the seatbelt, and then after a crash you want to tow the car, and after repair, you fix the car, so long as you define all these things, your system should be working correctly, but the real trouble is when your trying to define this elsewhere, if you're trying to define this in regular code, you have to have so many little things in there, like when we go from parked to anything, then we have to put on a seatbelt? You can't do a regular ActiveRecord update from the parked state to something else and just know that it's going to work. This state machine gem is really really helpful to make sure that you're getting things done, and like put_on_seatbelt method. This is also very awesome because it allows you to define which transitions are valid, so if you call the park event, then it will only transition from idling to first gear into parked, you can't take it from 4th gear, and immediately end up parking, that would be dangerous, so it helps you define all of the rules in a reasonable format, I think this is probably the best that I can think of, that you have used to define this in ruby, because state machines can be quite complex, and have quite a few rules, and really, this is long for an example implementation, but when it look at all of the stuff that it does, it really takes a lot of the vehicle required functionality of the vehicle, and defines it reasonably cleanly. It's a lot of code, but vehicles are also very complex. You can even see down here, I belive there's a second state machine for the car's alarm, which is really interesting, because you're going to have multiple state machines for the exact same object, which means that the car's alarm might be on, might be off, all of those things, and you can trigger that even from various other things like your other state machines. This is really interesting, and it allows you to say: When the car is parked, let's override the speed of the car and always force it to zero, which is really interesting as well, so you can take this stuff, and you can basically override these methods as necessary throughout your system. So the state machine gems are pretty fantastic, and there's anotherone called aasm which is very similar, it seems to be more recently maintained, I had to fork the state machine gem itself to include a few patches to work with the latest version of rails, but yeah, take a look at both of these, because they're also very similar but I think pretty fantastic, so this one defines states in a little different manner, and you can do pretty much the same things I believe, I haven't seen anything that you couldn't do in one or the other, it just kind of depends on the syntax that you want to support. Take a look at both of these, I'm going to show you how we can use the state_machine in rails, and take this and basically, layout what payments flow might look like with our state machine. I've got a payment's app here that I just created, and there's nothing in it, so the first thing that we have to do to set up our state machine is we need a model with an attribute for the state, so if we create a payment model:

rails g model Payment product amount:integer state
rake db:migrate

This will set up everything for the foundations of this gem, I'm going to go down here at the bottom and add the state_machine gem, and you can do the same with aasm, it's practically the same flow, I'm going to grab the GitHub version from my statemachine fork, just so we can have that, and we can run bundle to install that. Now that that is installed, we can open up the payment model

app/models/payment.rb

class Payment < ActiveRecord::Base 
    state_machine :state 
end 

:state is going to be the symbol for the attribute that you are saving to the database, our is called state, so that's great, and then you can set an initial value, and so ours might be: "pending" because we're working on payments. Maybe as soon as you create a new payment it's pending, and we know payments that have started, or maybe people who have started but they haven't necessarily gone through and started processing this, so we have probably a few states that we want to create. We want to have "pending", and "pending" can go to "processing", imagine this happens in a background worker or something, we can do the processing in the background, and from "processing" we can either have a "successful" payment, or we could have a "failed" payment, and then we can also "refund" the successful payment, but not a failed payment. This is sort of just the flow, and this is the state machine that really just flows in one direction, and doesn't really loop around or anything, so it's a more simple state machine, but it still has to go through these steps. It's important for us to set these events that are going to happen. So we'll have these events, and these will be the things that transition the state from one to another, so you want to have an event that happens. Basically you're going to do something like process

app/models/payment.rb

class Payment < ActiveRecord::Base 
    state_machine :state, initial: :pending do 
        event :process do 
            transition pending: :processing 
        end
    end 
end 

This is something that you can see here in these examples, you can transition from "idling" or "first gear" to "parked", you can transition from "stalled" to also "stalled" or "parked" to "idling". This is something that we're going to define here, so when you start to process a payment, you're going to go from "pending" to "processing", and then we're going to have events that our payments process. Imagine that you have some code in your background that's doing this. Once it's in the processing state, you can have a fail method, or a success, and the last one, we want to have a refund

event :fail do 
    transition processing: :failed 
end 

event :success do 
    transition processing: :successful 
end 

event :refund do 
    transition successful: :refounded
end 

All of these are really just going to transition the states on the object, so let's take a look at that on the rails console.

rails console
Payment.new

We've defined fail that is already on the object, I'm not exactly sure where it comes from, weather it's ActiveRecord::Base or the state machine, but the fail method is already there, so let's just change the event name to failed

Our state is automatically set to "pending", and if you'll notice, we didn't set a default in our ActiveRecord migration, so the state would by default be nil, except for in this case, because the state_machine gem automatically sets that up for us to set it to pending. We can say

p = Payment.new
p.process

This will save the record and transition it to that state, here we can see that when we call process it has transition to the "processing" state. This is maybe when your user sets the processing, or maybe your user puts the credit card in, you set this payment, and then it sets itself to processing, and then your background worker picks it up and says: Ok, now that we're processing, let's see if the payment fails or succeeds, then your code in the background can do one of two things. One, it could say: Ok, it failed, so let's tell it to transition the state to "failed", and when you call the p.failed, it will call this event and transition from "processing" to "failed". Now we can see that we have a failed payment, and if you were to try to do something like p.success, here, and transition it from "processing" to "successful", it won't succeed, and you will get "false" in return because the state that it's currently at failed doesn't allow that, and you can also do various things here, where you can ask the state machine if it can do something. Here's an example in their example README, you can see if you can do various things, so vehicle.can_ignite?, so we can say p.can_success?, and it will tell you false, and you could build these methods dynamically, you can see: Can we take this payment to the successful state? Well, no we can't because we're currently "failed", so it provides all these various helpers, and it shows you the transitions and the state events, and a bunch of stuff like that that allows you to use these helper methods in a way that it can generate them, and you can just interact with it in a more fluid manner. This is really cool, and we can create a new payment.

p = Payment.new
p.process
p.success
p.can_refund?

It will say "true" because we do allow that now. What it's doing behind the scenes is it is saying when you can refund, it looks for that event for the refund, and then it says: If your state is currently successful, then we can transfer you to the "refunded" state, and that's basically all it's doing behind the scenes. It's looking to see if it matches any of these transitions that you set up, and will tell you true or false if that is allowed. So it's really pretty simple behind the scenes, but it's got a lot of stuff set up for you. This is cool, we can call refund and that will refund the payment. There's a lot of things that you're probably thinking: ok, well you only transition these states and the only thing that changed on the object was the state, so how do I actually call the refund code, say from Stripe or Braintree when that happens, and you're basically going to set these up as these callbacks, so here's a couple examples of various things that you can do for those transitions or those callbacks. You have a couple helpers here to set up callbacks for before_transition, after_transition and around_transition, and you can set these up so that it will happen just like your before filters, your after filters and your around filters and controllers, but there's various things that you can do, so around_transition :benchmark I believe will call the benchmark method, this one here is a little bit different, and you can say: When you transition from "parked" to anything other than "parked", then call the put_on_seatbelt method and this after_transition says: When you go from anything to "parked", we'll just run this code right here which sets the seatbelt to off, so rather than setting up a put or like a take_off_seatbelt they have just done this inline with a block so you can do either one, and let's set up one of those in our application. Let's do:

app/models/payment.rb

state_machine :state, initial :pending do 
    after_transition pending: :processing, do: :process_stripe_payment 

    #rest of the state machine code 
    end 

    def process_stripe_payment 
        if true  
            success 
        else 
            failed
        end 
    end 
end 

In our console:

reload!
p = Payment.new

p.process

This should actually go from "processing" to "successful" for us, and it does, so what is happening then is when you call process, we go from "pending" to "processing", the state machine knows when you go from that transition from one to the other, you should try to process the Stripe payment, and then, in here it knows that well if it was successful, then let's transition to success, and if it failed, then we can transition to "failed", and this can all happen in a flow like that, and you can find your methods over here, outside of the state machine to do the processing itself, so after_transition*s and *before_transition*s are great when you want to do all of this stuff immediately when the transition happens, for example, if you wanted to do this in a background process, you could still write the same method, but you would rather than adding the *after_transition, you would delete this, and then you would process_stripe_payment from your background worker, which will just look for payments in the processing status for example. So this is great to do this in line, and then you can also do something like a before_transition, and you could say: Let's take successful payments, and when they go do a refund, we should process stripe_refund this is one of those cases where you actually want to do this before it fully transitions, because refunded would be the final state, and that would assume that it has been successful, so you want to do it before the transition happens, and we can o a process_stripe_refund, and this can be the one that tell it true of false like does it allow this to actually happen, so this could be some code in here that attempts the stripe refund, so it would be like

def process_stripe_refund 
    Stripe::Charge.retrieve().refund 
end

Once that's finished, as long as it didn't cause any problems, you would then be able to have this, go back and finish the transition, and so this would just run before it finishes, which means that if this fails or something like that, you can catch it before it actually marks it as refunded, so imagine that this actually raised a Stripe-like: We couldn't look up the charge, or it doesn't exist or something like that, so raise the Stripe error if you were to do that

reload!
p = Payment.last
p.refund

We would get something that would blow up, and that would be bad, but the good thing here is that even though it failed, it didn't go ahead and update our payment to "refunded", it caught it before that happened, so it would be definitely a bug in the system if your code crashed but it said it was refunded, and then your customer thought they were refunded but they really never were, that's a really dangerous bug to have in your code, so make sure that you pay attention to the proper transitions that you're doing here, because that is a crucial piece of writing good software, so you want to make sure that you're doing that stuff logically so that these transitions don't happen on accident, and that is the main thing for you to look out when you're writing a state machine, I'm going to make sure that all of the requirements are defined and processed successfully before the transition actually happens if that's the way that the transition should work. Since we could probably talk about this gem for hours, I want to touch on one last thing which I think is really useful which is the state blocks, so the state block example here is that we only really want to validate the presence of your seatbelt being on in the first or second gears, which means that if your car is parked, or it's idling, then we don't really care so much if your seatbelt is on or off, this is something that's really awesome because it allows you to do this rather than trying to put in if option in here with a lambda, and write a bunch of code in line, it allows you to define this in a much cleaner way, and we can do things that are really awesome, so let's get rid of this before_transition and the error that we're raising, but imagine that we want to have a method that only shows up when the payment has been refunded. Here we can simply say refunded state, and here we have defined a method. Maybe we have no "refunded at" column in the database, and we want to have like a refunded_at method here

app/models/payment.rb

state :refunded do 
    def refunded_at 
        Time.zone.now 
    end 
end 

This is as simple as it would have to be for this method to only show up during that refunded state, if you call this method and you're on "successful" state or "processing" or "failed" or "pending", it will throw an error because this method is not available, which is really nifty, and it allows you to organize your code, and then you can't call this method, so we have it already protected by putting it inside of the state block, if you didn't have it in here, you would have to move it out, and you would have to make this method down here, and first you would have to say

def refunded_at 
    return nil if state != "refunded"
    Time.zone.now 
end 

So you'd either have to do something like this, or you would have to raise an error, and that would be awful as well because now you're putting all this stuff inside the method, and it's like: Well, you're not supposed to be able to call this, so we need to protect it, and it just goes on an on, and it becomes kind of a painful thing where everyone of these methods that only applies to refunded has to do this, and this is the kind of code that you would have to write all over the place if you weren't using the state_machine gem, so imagine that you got rid of all of this, you would have to say

def process 
    railse StandardError if state != "pending"
    #you can't processs from the "successful" or "refunded" state 
    #processing....
end 

All of these methods you could define manually without the state_machine gem, but you would have to do all this stufff like that and you'd have to put in all those rules inside of your code, and then it becomes quickly like an unreadable thing, and that is the beauty of having the state_machine gem, because it allows you to define it, and all of the rules and it takes care of handling those exceptions and the transitions and everything about the validations between the two, without having to write all of your own checking code that could easily be written poorly and you can miss something, but this is defined in such a way that you don't have to worry about it, and the state_machine gem will take care of it for you, so that was kind of a whirlwind intro to the state_machine gem, I hope you enjoyed it, I definitely recommend using it, it does get a bit unwieldy once you start having a lot of events and things going on, so this is something that you may want to pull out of your model and put it in a concern and include it just so that you can make that model a lot more readable, it can be a little bit overwhelming because of how much code you have to write, but at the same time, it does allow you to protect the transitions of your objects throughout the system, and that alone is worth the headache having a couple hundred lines of code to define your state machine. It will probably take you more than a couple hundred lined of code to do this outside, and you're going to save yourself from a lot of problems by defining a state_machine in your rails app. I hope you enjoyed this episode, if you have a favorite over state_machine or aasm, let me know on the comments, and I look forward to talking to you next episode. Peace v

Transcript written by Miguel

Discussion