Close
Glad You're Ready. Let's Get Started!

Let us know how we can contact you.

Thank you!

We'll respond shortly.

LABS
Maintainable State Machines Part 1 – scopes and predicate methods

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.

Comments
Post a Comment

Your Information (Name required. Email address will not be displayed with comment.)

* Copy This Password *

* Type Or Paste Password Here *