State Transitions Are People Too
updated almost 6 years ago; latest suggestion almost 6 years ago
In this talk I present a simple ActiveRecord-based alternative to the many popular state machine gems.
Suppose you're dealing with a school application system. Applications can be submitted, rejected, approved. Then you would have
class Submission < StateTransition end
and so on. StateTransition is an STI subclass of ActiveRecord::Base.
Inside each 'concrete' transition subclass, AR validations determine whether the transition can be created, given current system state. Callbacks make changes to other models that result from the transitions, as well as trigger other effects like emails.
This shifts emphasis from the models to the transitions themselves. Model classes don't get cluttered with logic related to multiple transitions - that logic lives in the transition classes. It works nicely with REST. You are literally creating an Approval, rather than "approving". Logging of transitions is front and center. Nobody has to learn a new lib and you're never beholden to yesteryear's state machine gem-of-the-month.
With a few code examples to illustrate impact to Controllers, etc., this talk will take 15 minutes or less. Hopefully, Q&A from state machine fans will lead to some good discussion.
@james - Yes, I think that would set up discussion well. Points to touch on would include common state machine features, such as defining allowed transitions and aftereffects. It makes sense to look at some typical state machine code examples.
@murray - correct, this is not a gem, it is just AR + OO, which IMO is a virtue. Perhaps the main benefit of this approach is putting transition-specific logic together into a single dedicated class. It's in the spirit of SRP, and the resultant code ends up nicely factored. There's also an element of taking verbs and turning them into nouns, which leads to RESTful benefits and slim Controllers.
I don't propose that people should switch from gems, but rather consider this alternative moving forward. Using this approach, status-y attributes remain on the models, in support of efficient queries. However, the code that updates the status attributes is factored into transition-specific classes.
@paul RESTful benefits will be a focus of the talk. Given that you and Murray have both asked, I could perhaps address migration. However, I want to avoid the suggestion that users of state machine gems are "doing it wrong". Regarding performance, keeping a "status" field on the models preserves the ability to run efficient queries.
@tom the state is stored explicitly on the models and updated in before/after create callbacks on the Transitions. Keeping state on the models is pretty essential for query performance and simplicity. Modification of the Transition collections should be strictly additive - no updates or deletes - so updating model status during Transition creation should be sufficient for keeping things in sync. Unless you've found a hole somewhere :-)
@hakan thanks for the tip! Guess I should watch that Railscast if I end up presenting :-)
This Railscast may resonate with what you're writing. In the conclusion, Ryan rolls out his own minimal state-machine implementation by creating a simple has-many relationship from the Stateful Resource to its State.
I’m really interested in this. I guess the obvious question is how you represent the current state of each school application: is it a derived property of the transitions for that application (i.e. the newest one) or do you store it explicitly? If it’s stored separately, how do you keep the explicit state in sync with the implicit one, e.g. if the collection of transitions gets modified?
(I think I can guess the answers to these questions, but I’d love to validate those guesses and hear more about the trade-offs.)
I'm interested in this too. I think the state machine libraries people use are generally way overkill and actually make things more complex. So I think it would be interesting to explore hand-made approaches to the problem.
This idea definitely has legs, in my opinion.
This proposal came at a perfect moment for me, just before I started needing to think about state transitions in my current project. Instead of applying yet another dreary state machine gem with the inevitable screenful of transition guards and events, I've been applying this concept, and it's working out really well, especially after I made the transition actions properly RESTful.
It's also good advice beyond state machines, I think: identifying and extracting objects and resources leads to a happy and maintainable path. Even in Rails.
I've always been dissatisfied with my experiences with flavour-of-the-month state machine libraries. I've never considered modelling transitions as their own entities, but it seems to make a lot of sense.
This sounds like it's more than just a Rails idea, though. I'm sure that concrete examples of how to use it with Rails will be useful, but I'm more interested in understanding conceptually how it fits better with REST, and how it will improve my life (logging transitions without adding bureaucracy sounds nice, for example).
Like Murray, I'd also like to know if there's a practical path to move a state-machine-gem-based project to this design, but you seem to have rejected that.
Are there any performance snares to be wary of? How can they be mitigated?
Am I correct in thinking that this isn't a gem, it's an approach that you suggest people adopt?
You hint at it, but will you be sharing the reasons that you are promoting this way of doing state machines?
Most existing statemachine implementations have very similar APIs so changing from one to another is quite easy. I suspect switching to this approach isn't be as simple, would you be able to give some tips for how to transition* from a more traditional attribute-based approach to this one?
*: pun intended, lol
As a way of setting the context, and providing some motivation, would it be worth talking about the kinds of situations that a state machine is useful for?