Adding namespaced methods dynamically to a Class (March 07, 2009)
I'm working on a module that is meant to add some methods to a class when included. There's a standard idiom to do this:
module NiftyCapabilities
def self.included(other)
other.extend(ClassMethods)
end
module ClassMethods
def grab_banana
# ...
end
end
end
class Monkey
include NiftyCapabilities
end
When include NiftyCapabilities is evaluated in Monkey, the hook NiftyCapabilities.included is called with Monkey passed as a parameter. NiftyCapabilities.included then calls Monkey.extend(ClassMethods), which adds the instance methods of NiftyCapabilities::ClassMethods to the class Monkey. Since a class method in Ruby is just an instance method of a Class object, the instance methods in NiftyCapabilities::ClassMethods are now class methods on Monkey. Hooray for first class classes.
This standard idiom is great as far as it goes, but for this library I have one additional goal: to be as unobtrusive as possible. I'm concerned that if I add a Monkey.grab_banana method via this idiom I'll overwrite an pre-existing grab_banana method with unfortunate results.
The solution here is to borrow a principle from unobtrusive Javascript: you should add no more than one symbol to the global namespace. In this context, for "global namespace" read "class namespace." The natural way to do this is to add an inner module to Monkey and define these new methods in that inner module. Here's how:
module NiftyCapabilities
def self.included(other)
other.class_eval <<-END_CLASS_EVAL
module NiftyCapabilities
def self.grab_banana
# ...
end
end
END_CLASS_EVAL
end
end
class Monkey
include NiftyCapabilities
end
Now when NiftyCapabilities.included(Monkey) is called, that here doc is evaluated in the context of the class Monkey. The result is as if we had written:
class Monkey
module NiftyCapabilities
def self.grab_banana
# ...
end
end
end
Let's flesh out our banana-grabbing capabilities to absolutely satisfy ourselves that the modules created are perfectly cromulent inner modules of our classes:
module NiftyCapabilities
def self.included(other)
other.class_eval <<-END_CLASS_EVAL
module NiftyCapabilities
# Don't use module variables in real code if you can help
# it, OK?
@@banana_count = 0
def self.grab_banana
@@banana_count += 1
end
def self.banana_count
@@banana_count
end
end
END_CLASS_EVAL
end
end
class Monkey
include NiftyCapabilities
end
class Gorilla
include NiftyCapabilities
end
Which we can try out like so:
user@rubybox$ irb
irb(main):001:0> require 'namespaced_methods'
=> true
irb(main):002:0> Monkey::NiftyCapabilities.banana_count
=> 0
irb(main):003:0> Gorilla::NiftyCapabilities.banana_count
=> 0
irb(main):004:0> Monkey::NiftyCapabilities.grab_banana
=> 1
irb(main):005:0> Monkey::NiftyCapabilities.banana_count
=> 1
irb(main):006:0> Gorilla::NiftyCapabilities.banana_count
=> 0All content of this site copyright © 2009-2010 Phil Darnowsky. Released under a Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 Unported license. Some links in this blog may be affiliate links, which pay a small sales comission.