technicalpickles

Open Source ProjectsCode that Might be Useful to You

Talks I've GivenOn Technologies and Ideas

ThoughtsWhere I Sometimes Write Things

Resume If You Believe In Those

Follow Me On

GitHubIf coding is your thing

TwitterIf you tweet

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
  end

Fortunately, 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
  end

That’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
end

I 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?
  end

Having 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
  end

Looking 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?
  end

I’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.

comments powered by Disqus