June 12, 2008

Posted by John

Tagged beginner, class, scope, and variables

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
end

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

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

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

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

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

The 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

  1. 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.

About

Authored by John Nunemaker (Noo-neh-maker), a programmer who has fallen deeply in love with Ruby. Learn More.

Projects

Flipper
Release your software more often with fewer problems.
Flip your features.