“We’ll just add a flag in the database.”
I hate that phrase, it just sounds like a hack. I’ve seen databases with tables
that are nothing but flags. You just know that all through the code are a bunch
if statements, conditionally doing all kinds of things on those flags.
So a while ago, I decided to pretend databases didn’t support boolean data types. I refused to add any more flags to my tables, and instead make classes out of each of the flags.
For example, say we have users who have to confirm their account before they can log into the site.
Instead of this:
class User < ActiveRecord::Base end
and the table:
users (id, name, confirmed, confirmed_on) -- We also want the time when they -- confirmed their account
We have this:
class User < ActiveRecord::Base has_one :confirmation end class Confirmation < ActiveRecord::Base belongs_to :user end
And our tables:
users (id, name) confirmations (id, user_id, created_on)
In my mind, this was good because by using Rails’
created_on attribute I could
get the confirmation timestamp for free. I no longer had to write something
like this in my code:
user.confirmed_on = Time.now
After doing this for a couple of states, I realized this was dumb. I just
created a bunch of classes that had no interesting behavior, and complicated all
my queries with joins. So I refactored the code back, to use flags in the
users table. This was much simpler and better.
But what if some object’s behavior depended on what state it was currently in?
For example, say we have documents that start out in a draft state, move to a reviewed state and then finally to a published state. And what differs in each state is the validation that’s performed when you attempt to save the document. So the validation that gets performed on a document depends on its current state.
Let’s create a class for each state to be responsible for the state specific validation, and let the document collaborate with them. Forget about the specifics for each state’s validation, I’m going to just make up some differences.
So we’ve got:
class Document < ActiveRecord::Base has_one :state validates_associated :state def before_validation_on_create # apparently Rails does not set the foreign key until after validation self.state = Draft.new :document => self end end class State < ActiveRecord::Base belongs_to :document def validate raise NoMethodError, 'Must be implemented by subclasses to perform state specific validation' end end class Draft < State # in a draft state, documents must have a title def validate if document.title.blank? document.errors.add :title, "can't be blank" end end end class Reviewed < State # in a reviewed state, documents must have a grade (A, B, C, etc.) def validate if document.grade.blank? document.errors.add :grade, "can't be blank" end end end class Published < State # in a published state, documents must have a license (Creative Commons, etc.) def validate if document.license.blank? document.errors.add :license, "can't be blank" end end end
So whenever a
Document is first created, we put it automatically in a
state in the
#before_validation_on_create callback. And
somewhere we have a form that POSTs to an action in some controller to allow
someone to change a
# POST id=1&state=Reviewed def update @document = Document.find params[:id] # same hack as in Document#before_validation_on_create @document.state = params[:state].constantize.new :document => @document if @document.save redirect_to document_path(@document) else render :action => :edit end end
#constantize is a Rails helper method that will turn a String into its
corresponding class object, e.g.
Reviewed.constantize returns the
We use Rails’
#validates_associated to automatically validate the
State object (i.e. send it
#validate) in order to perform the
state specific validation whenever a
Document is saved.
Let’s take a step back and look at this code. So we’ve got
fine and 4
State classes. Do we need those 4
State classes? I mean all
we’re doing is validation, if there were more interesting behavior maybe they’d
be justified but let’s try to get rid of them.
So instead of using classes to represent states, let’s use boolean flags.
Our schema goes from:
documents (id, title, body, grade, license) states (id, type, document_id)
documents (id, title, body, grade, license, draft, reviewed, published) -- draft, reviewed and published are flags
State classes, it felt weird to write code like this:
if document.title.blank? document.errors.add :title, "can't be blank" end
Because that’s exactly what
ActiveRecord::Validations::ClassMethods#validates_presence_of does for us for
free (including the error messages). I know we can reuse
Now looking at the Rails’ doc for
#validates_presence_of I see it takes an
if keyword parameter that determines if the validation should proceed. I
think that’s what were looking for.
Let’s refactor this trash:
class Document < ActiveRecord::Base validates_presence_of :title, :if => :draft? validates_presence_of :grade, :if => :reviewed? validates_presence_of :license, :if => :published? def before_validation_on_create self.draft = true end end
Now that’s better.
if keyword parameter gives us our
state specific validation.
And that controller action to update a
Document object’s state becomes:
# POST id=1&reviewed=1 def update @document = Document.find params[:id] if @document.update_attributes params[:document] redirect_to document_path(@document) else render :action => :edit end end
Boilerplate and simple. The rewritten version gets rid of that
hack in order to force the association before validation takes place:
In the action I had:
@document.state = params[:state].constantize.new :document => @document
self.state = Draft.new :document => self
I didn’t like that hack, so I’m glad I was able to get rid of it (If anyone knows a workaround I’d be glad to try it out).
So at first, I implemented the various states a
Document could be in using
classes, just like I did my
Confirmation feature because I didn’t want
to use boolean flags in my database. Then I decided that boolean flags are all
right, and simplified the code a lot by eliminating 4 classes. The first design
was an implementation of the classic GoF State pattern but in this situation the
State pattern is almost supported by Rails by using
if keyword parameter. That’s not to say the first implementation is not
applicable in Rails, it’s just that in this example that only dealt with simple
validation, it wasn’t appropriate. If each state had a lot more complex
validation and/or interesting behavior, then I would support the first
implementation of using classes for each state.