“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
# 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 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 Document
’s state:
# 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 Reviewed
class object.)
We use Rails’ #validates_associated
to automatically validate the Document
‘s
associated 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)
to this:
documents (id, title, body, grade, license, draft, reviewed, published)
-- draft, reviewed and published are flags
In our 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
And in Document#before_validation_on_create
:
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 User
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.