Make Sure to Properly Scope Your Ruby Mixins
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 refresh_totals 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
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:
That solved the problem. And now Scrawlers is much happier.