December 28, 2010
Older: Improving Your Methods
Newer: Year In Review
A Scam I Say!
Today, I repeated myself in a particular way for the last time. In at least four places in Harmony, I had faux classes that responded to all, find, create, etc. and initialized attributes from a hash. As I went to write another, I realized I had a problem and it was time for a new open source project.
A Little History
Harmony has a Site model, which has a site mode. The site mode is either ‘development’ or ‘live’. Back in the day, I would have created a SiteMode model that was backed by the database and hooked up all the relationships between Site and SiteMode.
Over the years, I have realized that is a waste. The information in that database table rarely changes and if it does, it is usually accompanied by other code changes and a deploy. This type of information is perfect for just storing in memory. If you need to change it, do so, commit, and deploy.
When I was originally working on site mode’s a few years back, I created a fake model that looked similar to this:
class SiteMode
cattr_accessor :modes
@@modes = [
{:id => 1, :name => 'live'},
{:id => 2, :name => 'development'},
]
def self.[](id)
SiteMode.new(id)
end
def self.all
@@modes.map { |m| SiteMode.new(m[:id]) }
end
attr_accessor :id, :name
def initialize(id)
self.class.modes.detect { |m| m[:id] == id }.each_pair do |attr, value|
self.send("#{attr}=", value)
end
end
def password_required?
id == 2
end
def display_name
name.titleize
end
end
With just that code, Site could belong_to :site_mode
and everything just worked. I got my same relationships and instead of querying a database (and having to keep that data in sync), everything was just stored in memory.
The Scam
Like I said, rather than create another fake model with all the same code and tests, I pulled out the shared pieces into a gem named scam. With scam in the mix, the SiteMode model now looks like this:
class SiteMode
include Scam
attr_accessor :name
def password_required?
id == 2
end
def item_cache?
id == 1
end
def display_name
name.titleize
end
end
SiteMode.create({
:id => 1,
:name => 'live'
})
SiteMode.create({
:id => 2,
:name => 'development'
})
By just including Scam, we get all the enumerable functionality and such (see the specs for more). Now the SiteMode class deals specifically with site mode related code instead of how to initialize, create, and enumerate site modes. I switched the other classes that used the same idiom and was left with less code on the other side.
A Few Notes
This is nothing earth shattering, but it saves queries and wraps up an idiom I was using into a separate, well-tested piece of functionality that I can share not only across Harmony, but other applications as well.
One other thing worth noting is my choice of using id’s and having them be integers. The first advantage to using integers is size. The integer 2 is smaller to store than the string ‘development’. That might not seem like a big deal, but after working on the projects I have recently, every byte still counts.
The second reason is that integers are more flexible. Right now, 1 is live and 2 is development. If I decide I want another site mode to be the default instead of development, I can simply change development to a different id and create my new one as 2. The new one instantly becomes the new site mode for all sites with site_mode_id of 2.
If, on the other hand, I had used strings, I would have to map my new site mode to development and map development to something different. This would certainly lead to confusion in the code down the road. Strings have meaning because they are often words, whereas integers do not. Hope that makes sense.
At any rate, you can gem install scam
if you have a need. If not, I hope some of the ideas in this post inspire you in some way.
10 Comments
Dec 28, 2010
Was there a specific reason you chose not to inherit from ActiveModel ?
Dec 28, 2010
@Steven Soroka: Yep, Harmony is Rails 2 still. Scam as it is will work with both 2 and 3. Also, since I am creating the data, I do not need validations, callbacks, etc.
Dec 28, 2010
I’m rather new to Rails still… But I don’t quite understand why you have these models in the first place?
Dec 28, 2010
Are you persisting only mode_id to the Site model? In this case, what happen if you want to do stats reporting on your MongoDB Site collection and can’t use Ruby for example.
Dec 29, 2010
I do this sort of thing a lot, too. It’s nice to see someone else share the same thoughts I do for situations where the data rarely changes. I’m using this approach in a roles/permissions plugin I wrote.
Also, have you considered using OpenStruct for things like the
@modes stores? @
modes = [OpenStruct.new(:id => 1, :name => ‘live’), …] and then you could access it with SiteMode.modes.first.name and so on. Nearly every time I take this approach, ostruct finds a way in the mix :-)Dec 29, 2010
I released something similar 6 weeks ago: https://github.com/niko/is_a_collection.
It’s more limited in what it does: no attribute initialization, no #create. It just keeps collection index.
Dec 29, 2010
This looks pretty neat, thanks for releasing it!
To Ryan’s point, this may be somewhat cargo-culted knowledge, but we found that OpenStruct had some serious memory leak issues (at least back with ruby 1.8).
Dec 29, 2010
@Christian W: Mostly to move responsibilities around. You can either have a ton of if/else or case statements in your site model or have a separate site mode class and delegate functionality to it.
@mech: Yep, only the mode gets persisted in site. If I can’t use Ruby I can just code the integers in a js function as well for mongo to use. I really do not see it as an issue.
@Ryan: I like defining accessors as then there is never confusion on what is available. To me, open structs can be like having a schema-less database with no application schema either. It works, but it is not always fun to maintain.
Dec 30, 2010
Any reason why you didn’t just use https://github.com/zilkey/active_hash instead of writing what seems to be the same functionality?
Dec 30, 2010
Yeah, I use ActiveHash, but it aims to be closer to a real ActiveRecord class, including a #save method, find_by_* magic, and associations (optionally). Scam looks like it’s about 10 times simpler, so I guess if you didn’t need all that stuff then Scam would be a perfect fit.
Sorry, comments are closed for this article to ease the burden of pruning spam.