learns_to Modules and Namespaces: Lessons from Wrapping the del.icio.us api

10 September, 2006

Last night, I started working on putting together a Ruby-wrapper for the del.icio.us api. I need it to execute this little idea I had recently (more about that when it's done) and I was surprised to find that there wasn't anything too useful out there -- though it's probably because the api is so easy to use you barely need a wrapper around it for most projects. There were a few libraries, but nothing really clean and complete and nothing using the new v1 of the api.

Anyway, in the course of working on the wrapper, I came across a common problem: the need for multiple namespaces. In the api, method names are not unique across objects. For example, there's a method that gets posts for a user and one that gets tags, both called "get" (api.del.icio.us/v1/posts/get and api.del.icio.us/v1/tags/get, respectively). Obviously, those urls leave no confusion as to which "get" method gets which type of object. The question is: what device in ruby should I use to capture this with equivalent clarity?

Two strategies occurred to me immediately: modules and subclassing. According to the relevant section of Programming Ruby, "modules are a way of grouping together methods, classes, and constants. . .[They] provide a namespace and prevent name clashes." Well, that sounds like exactly what I want to do. I want to group together the api methods for posts so that they don't pollute the namesapce for tags. Under this design, I would have multiple modules within my main class, one with the methods for each api "object," posts, tags, bundles, and whatnot.

So, to see if this would actually work, I ginned up a simple example of using modules inside a class. This is what it looked like:

#namespaced class methods
class Test
module Gar
def self.to_s
puts "gar!"
end
end
module Bax
def self.to_s
puts "bax!"
end
end
end
Test::Gar.to_s
Test::Bax.to_s

If you ruby this you'll see this output:

gar!
bax!

In other words, it seems to work for class methods.

But what about instance methods. I made my toy example a little more complicated:

class Best
attr_accessor :dog
def initialize
@dog = "bot!"
end
module Gar
def self.set_dog
@dog = "gar!"
end
end
module Bax
def self.set_dog
@dog = "bax!"
end
end
end
t = Best.new
puts t.dog
Best::Gar.set_dog
puts t.dog
Best::Bax.set_dog
puts t.dog

Unfortunately, this doesn't seem to work. The modules can't get access to the instance variable, @dog. The output ends up looking like this:

bot!
bot!
bot!

This means that I'm thrown back to trying to solve the problem with regular subclassing. I'll be defining a series of classes like this:

class Relicious
attr_accessor :username, :password
#my main class, connects to del.icio.us, etc.
end
class Post < Relicious
def get
#call the posts/get url
end
end
clas Tag < Relicious
def get
#call the tags/get url
end
end

That way, each separate subclass can implement identically-named methods with no danger of namespace confusion. My initial instinct was that this pattern was slightly less elegant than what I was trying to achieve with modules because the subclasses all have to access the centralized connection methods and such in the parent class. The resulting usage code looks like this:

post = Post.new
post.username = "myusername"
post.password = "mypassword"
post.get

which is ugly (a post doesn't really have a username) and inefficient (you'd have to set the username and password attributes fresh if you called Tag.new since you'd have a new instance).

Thankfully, today Chris proposed a better solution, which, in retrospect, should have been obvious to me: wrapping up the child objects inside of accessors in the parent class and then only ever accessing them from there. This would turn the above usage code into this:

rel = Relicious.new
rel.username = "myusername"
rel.password = "mypassword"
posts = rel.posts.get

The namespace problem is solved, everything is meaningfully encapsulated, and the syntax is concise and clear. Sounds like good design. Now, all that's left is to actually implement it. . .

Tagged: , , , , , ,