Just Read The Directions
(Because sometimes that helps.)
Articles on programming by Phil Darnowsky.

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 => 0
blog comments powered by Disqus