Tuesday, April 29, 2008

An Optional Require for Ruby

In the fine balance between code organization and keeping the system quiet, I like to err on the side of silence. I just don't like warnings and noise coming from the compiler and runtime, and will sometimes code around the squeaky parts of a system so I don't have to listen to the racket. Despite the messiness of my physical reality, I like to keep my virtual reality well-organized and clean. That goes for my code too - I don't like writing Goldbergian code unless it's for play. I like to put the right code in the right place.

I write most of my Java code in Eclipse these days and there are little noisy warnings that seem to pop up everywhere. While they can be supressed or filtered, I kind of hate it when I have to refactor correct code and move things around because of the judgement of a coding tool. Happily, I don't have to in Ruby. Ruby doesn't have a "compile time" and unit testing removes the stray vibrations from the code, so it's almost always nice and quiet. Plus, the concept of "where things go" is never tainted by file boundaries - code for any class can go into any file and Ruby sorts it all out.

Until yesterday.

I was writing some code to open html documents, keeping everything organized together so the functionality was all in place. The code was intended to open the html in the user's selected browser on any platform so I could call one high-level method and have it just work. To do this on Windows I needed the win32/registry feature. So I added it with a require and everything was fine. I was happy. Later I pulled the code onto my iMac and ran it. LoadError.

I was aghast. My Ruby was befouled. Of course, Apple has no need for Windows Registry access, and so was missing the feature I had required. Fine. I could install the gem anyway and move forward. But then I thought, this is code I want to use in production - customers would be using this and there is no need for them to load that gem if they were on Apple or Linux.

So I was faced with two evils. Pull in the gem or code around it. I decided to code around it. I then had to decide whether to break up the module and only include the windows part if needed, or keep everything together so the logical functionality was colocated. I decided to keep everything together. This now meant that I needed some conditions around my require.
require 'win32/registry' if
Config::CONFIG["target_vendor"] == "pc"
That's fine - everything was back on track.

I did get to thinking that I really was working too hard, and that I'd end up repeating myself again somewhere down the road. I wanted something more general, that would give me a pass if a LoadError occurred, and handle the missing piece later somehow. I decided to create an optional require.
class Object
def optional_require(feature)
begin
require feature
rescue LoadError
end
end
end
Now if I used it and got a LoadError, everything would just move on.
optional_require 'win32/registry'
This removed the guts of the decision from the actual require, and expected the require to fail on the platforms that didn't need it. It also left the door open for writing a 'check_configuration' method that would make sure that the user's feature set was complete when an application starts, which to me is just good practice. I was happy enough with this solution to place it into the eymiha-0.1.3 rubygem, available from in my cori project (Chunks Of Ruby Infrastructure) on rubyforge.

So, less noise and more organization. Just what I was looking for.

1 comment:

John said...

Hey thanks for this, just what I was looking for