Constantly Hacking Ruby Constants
Here at BreakingPoint, we write all of our application simulation code in Ruby. Lately, I've been working on adding a slew of new behaviors to our IMAPv4 implementation so users can fine-tune their IMAP Application Simulator and Client Simulator flows. At first, this seemed like it would mean a whole lot of typing to wire up twelve new actions.
Instead of copying and pasting all over the place (and dreading the possibility of fixing the same bug in fifteen zillion places), I needed to come up with a code reuse technique that takes advantage of the existing codebase written using standardized naming conventions. Since I'm swimming in these standard names, I figured there must be a way to use Ruby's dynamic typing and extensible classes to make this easier on myself, both now and in the future.
The first trick is to programmatically figure out which application profile class to use when I'm in a particular protocol. For example, if we're in a function in the "Imap" object, I need to get protocol configuration from the "ImapProfile" singleton object. This is pretty easy with Ruby's introspection and the nifty Kernel.const_get() function.
So, let's say we have a (simplified) ImapProfile class:
class ImapProfile
def self.config
{:username => "todb",
:password => "Shadowfax" # Unguessable!
}
end
end
In the Imap subclass of Application Manager, I'll want to get a hold of those configuration parameters. I can do so with something like this:
module AppManager
class Imap
# Get my class name, strip off the superclass
def my_protocol
self.class.name.to_s.split("::").last
end
# get_profile_params() takes the string from
my_profile_object(),
# gets the associated constant, and invokes
the config() method. def get_profile_params
Kernel.const_get(my_protocol + "Profile").send :config
end
end
end
Now we can call the profile object's "config" method by deriving the class name from our own class's name:
irb(main):001:0> @app = AppManager::Imap.new
=> #
irb(main):002:0> @app.get_profile_params
=> {:username="todb", :password=>"Shadowfax"}
That's pretty neat and all, but the real trick is to figure out how to do the same thing with a method name, since (as you'll see) they bear a resembelence to individual action classes. After a little bit of research, it turns out we can perform something similar with the Kernel.caller() function, and again use some string manipulation to get what we want:
def caller_action_to_constant
caller[1] =~ /`([^']+?)'/
$1 =~ /^do_(client|server)_(.*)/
$2.split("_").map {|s| s.capitalize}.join
end
This function takes the second element of the execution stack, extracts the calling method's name (the first regex), extracts the part we care about (the second regex), then splits on the underscores in order to CamelCase the result. In the end, the string:
"do_client_send_user_name" becomes SendUserName
Why not the first element of the call stack array? Well, I'm wrapping this up in an intermediary function, called the action_executor, which takes this string and performs another const_get to actually use it for something:
def action_executor(args={})
Kernel.const_get
(my_protocol + caller_action_to_constant + "Cmd").send :data
end
So, from now on, the do_ actions can call the action_executor in order to track down the right classes to get the data from:
def do_client_send_user_name(args={})
action_executor(args)
end
Pretty neat, if you ask me. A complete code listing should be available here, at Pastie.
In the end, this strikes me as an implementation of the OO Delegation design pattern. However, it includes some extra smarts about where the delegatee is, all based on a common naming convention for classes and methods. While the example code is sparse, in reality, the application actions I'm replacing in IMAP were each around 15 lines, and this technique compresses them down to one. I also get the added bonus of centralizing a common function to one spot, to ease future tweaks to the way application protocols work, or, the laughably remote possibility that there's ever a bug discovered there.
blog comments powered by Disqus