In this post I’ll point out a few ways to write maintainable scopes and predicate methods on ActiveRecord objects that use state machines.
Imagine you work for a small e-commerce site that deals with various products that could be in stock or out of stock. You might start out with a class that looks like this:
class Product state_machine :status do state :in_stock state :out_of_stock end end
Your CEO says that you should only ever display products that are in stock, so you add a scope and a predicate method to help out with that:
class Product scope :in_stock, where(:state => "in_stock") state_machine :status do state :in_stock state :out_of_stock end def in_stock? status == "in_stock" end end
Your site meets with wild popularity, and soon you have a thriving consumer site, an API and a mobile site. You have dozens of places where products are displayed, and all of them show products that are in stock. Then your CEO tells you that the site should now show products that are in stock OR out of stock, but not products that have been discontinued. Further, you have to have this done by tomorrow, because there’s a press release going out.
You have a catalog of 20 million products, and changing the state name will cause the site too much downtime, so you hack together something like this:
class Product scope :in_stock, where("state in (?)", %w(in_stock out_of_stock)) state_machine :status do state :in_stock state :out_of_stock state :discontinued end def in_stock? %w(in_stock out_of_stock).include?(status) end end
It’s not pretty but it gets the job done. Now the term “in_stock” in your domain and in your database mean different things, and getting out from under it may be expensive.
To avoid problems like this, I think it’s a good idea to avoid parallel names in states, scopes and predicate methods. In each piece of the code that needs to inquire about an object’s state, ask yourself “why am I asking about the state of the object?” For example, a page that lists products is asking about state because it wants to know “is this object available for purchase?” whereas the warehouse admin pages might be asking “do we need to reorder this product?” In these cases, you might end up with a class that looks like this:
class Product scope :available_for_purchase, where(:state => "in_stock") scope :needs_reorder, where(:state => "out_of_stock") state_machine :status do state :in_stock state :out_of_stock end def available_for_purchase? status == "in_stock" end def needs_reorder status == "out_of_stock" end end
In this case, if you need to make a change to the meaning of
available_for_purchase you can do so without having to change state names in the database or change code anywhere else in the system. In the beginning you may end up with multiple methods or scopes that point to the same scope, but there are several ways to dry up the implementation of each method to stay DRY.