Early on in writing Scrawlers I realized I needed to improve the manner in which we calculated our bestseller list. In true Agile(tm) form, I had built the site to dynamically calculate the bestsellers at the given moment a reader queried. Not so bad with ten stories. Not so good otherwise, especially since this method precluded paging of the results.
Ruby mixins to the rescue
It was clear that the code necessary to calculate top rated stories would be similar for each duration: all time, monthly, and weekly. The technique was to populate summary tables using scheduled jobs running in the background. Each duration would get its own table. Separation of this data would lead to simplicity.
With a basic HOWTO in hand, I sought to learn the techniques I could leverage in Ruby to DRY up my code a bit. The Java-me would fall into the abstract class model, but Ruby on Rails is my new world. I settled on a mixin technique - mixing a module with a class. Specifically, my classes would “extend” the module, which essentially makes the module methods class-level methods, rather than object-level methods.
When mixins conflict
Unfortunately things did not go as smoothly as I would have liked. My mixins began stomping on each other. First, take a look at the generalized code in my module:
module RatingTotals::StoryRatingTotals
def refresh_story_totals(date = Date.new(2007, 1, 1))
# Removed most the code.
ratings = Rating.find(:all,
:select => 'rateable_id, AVG(rating) as average_rating, COUNT(id) as count ',
:conditions => ["rateable_type = 'story' AND created_at >= ? ", date],
:group => 'rateable_id ')
totals = []
ratings.each do |rating|
totals << self.new(:story_id => rating.rateable_id,
:rating => rating.average_rating,
:count => rating.count)
end
self.transaction do
self.delete_all
totals.each do |total|
total.save
end
end
end
end
As you can see, I collect all the ratings for the duration. Then I create total lines, which will be of class types for which the module is mixed in (see below). I do all of this in memory. Finally, a transaction deletes the preexisting totals and writes the new total lines to the summary table.
An example of one of the associated classes:
class RatingTotals::WeeklyStoryRatingTotal < ActiveRecord::Base
belongs_to :story
extend StoryRatingTotals
def self.refresh_totals
self.refresh_story_totals(1.week.ago)
end
end
Seems simple enough. Extending my module, I have access to a class-level refresh_story_totals method. Cool. Or not.
When testing through the console, I could only execute a single one of the class methods ( e.g. MonthlyStoryRatingTotal.refresh_totals). Whichever class method I invoke first can be executed ad naseum, but if I try to execute one of the other class refreshtotals methods they are blocked out with the most classic of error messages:
ArgumentError: RatingTotals is not missing constant StoryRatingTotals!
When mixins error, check your scope
If you looked at those class and module definitions closely, you may have noticed that I have an extended scope (e.g. RatingTotals::StoryRatingTotals). Yes, these models are stored in a “rating_totals” directory. Since my problem involved the code “half” working, I had a lot of trouble uncovering the source of the problem. If the errors were complete and total, I think I would have found the issue rather quickly.
The problem is that in my “extend” definitions in the classes, I did not properly scope the module reference. I should have:
extend RatingTotals::StoryRatingTotals
That solved the problem. And now Scrawlers is much happier.
