Refactoring an ActiveRecord callback
Inspired by a few articles and pesentations (1 2 3 4), I decided it was time to cleanup some of the logic in my Post model related to a particular ActiveRecord callback. The fact that I needed some comments to explain what it is doing should be a red flag.
before_validation :update_published_at_if_necessary
def published?
self.is_published == true
end
def unpublished?
! published?
end
protected
# Ensure that published_at is set accordingly.
#
# * Unpublished posts should not have this set
# * Published posts should have it set to the current time
def update_published_at_if_necessary
if new_record? && published? && published_at.nil?
self.published_at = Time.now
end
if ! published? && !published_at.nil?
self.published_at = nil
end
endFortunately, I have tests in place that excercise this logic, so as long as my tests are passing, the refactorings must work (hopefully!).
My first impression is that the callback is doing too much work. Let’s split it up into two pieces.
before_validation :set_published_at_to_now_if_necessary
before_validation :unset_published_at_if_necessary
protected
def set_published_at_to_now_if_necessary
if new_record? && published? && published_at.nil?
self.published_at = Time.now
end
end
def unset_published_at_if_necessary
if ! published? && !published_at.nil?
self.published_at = nil
end
endThat’s somewhat better. At least the methods are more focused.
Hmm, I probably don’t need to check the existing published_at value when something isn’t published. I can also make use of unpublished?
def unset_published_at_if_necessary
if unpublished?
self.published_at = nil
end
endI would do something similar for set_published_at_if_necessary, but I don’t want to override the published_at if it was explicitly set. Maybe I post something in the future, or past. I go a little crazy sometimes with that.
I could probably simplify the conditional logic in set_published_at_if_necessary by making a new method.
protected
def set_published_at_to_now_if_necessary
if new_published_post_without_published_at?
self.published_at = Time.now
end
end
def new_published_post_without_published_at?
new_record? && published? && published_at.nil?
endHaving if_necessary into the method names are kind of bugging me. before_filter supports :if and :unless options, so we should use those, and remove the conditionals from the callback methods.
before_validation :set_published_at_to_now, :if => :new_published_post_without_published_at?
before_validation :unset_published_at, :if => :unpublished?
protected
def set_published_at_to_now
self.published_at = Time.now
end
def unset_published_at
self.published_at = nil
endLooking good. Looking pretty, pretty good.
Let’s see the finished product:
before_validation :set_published_at_to_now, :if => :new_published_post_without_published_at?
before_validation :unset_published_at, :if => :unpublished?
def published?
self.is_published == true
end
def unpublished?
! published?
end
protected
def set_published_at_to_now
self.published_at = Time.now
end
def unset_published_at
self.published_at = nil
end
def new_published_post_without_published_at?
new_record? && published? && published_at.nil?
endI’m pretty happy with this. Reads really well. Some of the methods have kind of long names, but I can deal with that.
The only part I don’t really like is published? and unpublished?, but that’s for another day.