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
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
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
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.
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
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
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:
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:
p = Payment.new
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
p = Payment.last
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
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
Transcript written by Miguel
This was really good I love hearing/learning/ finding out about these little things that I dont think about or know about yet being a fresh programmer. From an experienced programmer, what would maybe be a cool app to make to help with practicing this even if it was just in one ruby file.
I think coming up with a simple 'idea' or 'game' to build to practice the idea comes so hard so that would be helpfull thanks man for all your services!
A crazy in-depth example is this for video games: http://jessewarden.com/2012...
They are a fantastic use case for state machines because you've got to keep track of so many things when it comes to power ups. Take just the ghosts in pacman for example: http://oddwiring.com/archiv...
I would say that if you want to try this out, try building a soda machine where you type in the various coins you put into the machine and it counts up and dispenses the soda (and change) once you've hit the correct price. You can just write these events out as strings into the console and that would be a good first experience with it.
I think https://github.com/state-ma... is the latest version for rails. Why don't you use it?
I think you are right! I have just not used the gem that often since the last time and this wasn't available then. It does seem to be a community supported version that is the most up to date. Thanks for sharing!
Hi Chris! Would it be possible to do more videos on state machines in regard to multiple conditions for each event and also something on views? Thanks a bunch!