June 12, 2008
        Older: Programmers Should Give Up More Often
        Newer: Alias Attribute
      
A Class Instance Variable Update
So you people love yourself some class and instance variable explanations. Over a year and a half ago, in a mildly confused state, I worked through the weirdness of class and instance variables in ruby and posted my findings. To this date, it is by far my most trafficked post here on Rails Tips and gets almost time and a half the views that my entire home page does. On that note, I decided to post an update as I’ve changed the code a bit to not conflict with rails and I’m actually using it in one of my gems now.
Without any further ado, here is the updated snippet. The only change is I made the name of the instance variable less common so that it doesn’t conflict with Rails.
cattr_inheritable
module ClassLevelInheritableAttributes
  def self.included(base)
    base.extend(ClassMethods)    
  end
  module ClassMethods
    def cattr_inheritable(*args)
      @cattr_inheritable_attrs ||= [:cattr_inheritable_attrs]
      @cattr_inheritable_attrs += args
      args.each do |arg|
        class_eval %(
          class << self; attr_accessor :#{arg} end
        )
      end
      @cattr_inheritable_attrs
    end
    def inherited(subclass)
      @cattr_inheritable_attrs.each do |inheritable_attribute|
        instance_var = "@#{inheritable_attribute}" 
        subclass.instance_variable_set(instance_var, instance_variable_get(instance_var))
      end
    end
  end
endSo wait, I’m actually using code that I wrote? Yep, it’s true. I’m occasionally working on an Amazon Associates Web Services gem and due to the similarities of the requests when hitting the API, I decided to give my code a go-round.
Make a Base
The way I did it might be poor form, I don’t know, but it worked great. Basically, I created a Base class with some default parameters that get passed with each request to Amazon (in the query string).
module AAWS
  class Base
    include ClassLevelInheritableAttributes
    
    cattr_inheritable :default_params
    @default_params = {:service => 'AWSECommerceService'}
    # other code removed for clarity ...
  end
endThe Base class has a get method that automatically appends the default params to every request.
module AAWS
  class Base
    # code removed for clarity ...    
    def self.get(options={})
      # code removed for clarity ...
      params, options = {}, default_params.merge(options)
      options.map { |k,v| params[k.to_s.camelize.gsub(/^aws/i, 'AWS')] = v }
      connection.get('/onca/xml', params) # see right here they get added, yay!
    end
    # code removed for clarity ...
  end 
endItem < Base
So at this point I could make requests to Amazon’s web service but that isn’t much help unless I’m actually searching for something. Next, I implemented the Item class by inheriting from Base and tweaking the default params that get passed with each request.
module AAWS  
  class Item < Base
    @default_params.update :operation => 'ItemSearch'
    # code removed for clarity ...
  end
endNow, because Item inherits from Base, it has the get method and because the default params are updated, any request from Item will have operation=ItemSearch in the query string. Take the following calls for example:
AAWS::Base.get(:title => 'Ruby')
AAWS::Item.get(:title => 'Ruby')The only difference between them is that the Item one has the extra operation query string parameter. This isn’t the cool part though, it’s just the lead in.
Book < Item
The cool part is when you want to create a class that searches a particular Amazon index. For example, let’s say you don’t want to search all the different types of items but rather just books. With the way things are setup, it is really easy to do this.
module AAWS
  class Book < Item
    @default_params.update :search_index => 'Books'
  end
endNo code has been left out to make things clear. That is it. I simply update the default parameters again, this time to include the search_index query parameter. Updating the default params also includes the operation query parameter that was added in the Item class because Book inherits from Item. Now I can get an xml response for all items or just for books by using the calls below.
AAWS::Item.get(:title => 'Harry Potter')
AAWS::Book.get(:title => 'Harry Potter')The first will match DVD’s, books and anything else that is an item Amazon sells, but the second will only match books.
Cleaning Things Up A Bit
Ideally, I should wrap that default_params in a method so that you do something like this to change them rather than modifying the class instance variable directly:
module AAWS
  class Book < Item
    update_default_params :search_index => 'Books'  
  end
endThe update_default_params method would just implement that actual merging of the current defaults and the new ones being passed in as arguments. That feels a little cleaner and will happen down the road but I thought I would show one quick example of how I’m using class inheritable instance variables. I’m not heavy into design patterns so maybe there is a more “proper” way of achieving the same affect I just showed. If so, don’t be bashful. Let me know.


1 Comment
Jun 13, 2008
You might want to check out the extensions Rails (and Merb) adds to Class
It seems like class_inheritable_hash might be similar to what you’re implementing here.
- Brandon
Sorry, comments are closed for this article to ease the burden of pruning spam.