true or false
“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 of ‘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 self.state = Draft.new :document => self # apparently Rails does not set the foreign key until after validation 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
Draft state in the
Document’s #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] @document.state = params[:state].constantize.new :document => @document # same hack as in Document#before_validation_on_create 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
Reviewed class object.)
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
Document, that’s 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 #validates_presence_of somehow.
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. #validates_presence_of’s ‘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
:document => 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 #validates_presence_of’s ‘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.