Problem
You are designing an API. You want to include some small class which has dependencies the rest of your library does not have, and whose use is only optional, but you don't want the end user to have to require the files manually.
Solution
class XMLTracer
def self.new
puts "loading..."
require 'rubygems'
require 'builder'
rescue LoadError
raise LoadError, "must have builder installed..."
else
def self.new
puts "new is the new new"
super
end
new
end
def initialize
@xml = Builder::XmlMarkup.new(
:target => STDOUT,
:indent => 2
)
end
def identity
@xml.instruct!
@xml.comment! "Crepuscular Homunculus :: " +
"Advanced Ruby Tips and Tricks, Part 1"
@xml.class do |c|
c.name self.class.name
c.id object_id
end
end
end
x1 = XMLTracer.new
x2 = XMLTracer.new
puts
x1.identity
x2.identity
Which Outputs
loading...
new is the new new
new is the new new
<?xml version="1.0" encoding="UTF-8"?>
<!-- Crepuscular Homunculus :: Advanced Ruby Tips and Tricks, Part 1 -->
<class>
<name>XMLTracer</name>
<id>-607742088</id>
</class>
<?xml version="1.0" encoding="UTF-8"?>
<!-- Crepuscular Homunculus :: Advanced Ruby Tips and Tricks, Part 1 -->
<class>
<name>XMLTracer</name>
<id>-607742198</id>
</class>
Walkthrough
The first odd thing you might notice, which you don't see in the wild that often, is that the body of our self.new method contains a rescue and else clause. This is possible because method bodies implicitly are begin end blocks.
The other slightly odd thing if you're coming from more static languages is that the require statements are actually just methods and so can be called dynamically at runtime. What we are doing here is calling require only if the end-user creates a new instance of our XMLTracer class. If for some reason the requires fail, we'll catch and rethrow the load error.
def self.new
require 'rubygems'
require 'builder'
rescue LoadError
raise LoadError, "must have builder installed, blah blah blah"
Here is where it starts to get interesting. The code after the else clause is only run if no exceptions are thrown, so we know that builder has been successfully loaded. At this point we could just call super because require is smart enough not to reload previously loaded files, but that represents some unnecessary overhead every time a new XMLTracer is instantiated.
else
def self.new
puts "new is the new new"
super
end
new
end
Ruby's dynamic nature means that methods can be re-bound at runtime, so we redefine the new method to just call super, and then we call it explicitly to return a new instance. But from then on calling XMLTracer.new will not attempt to require anything. Which you can see in the output, where "loading..." is written only once.
Well, that wraps up this first installment of ARTT. Hope it has been of some use to you. If you have any suggestions/requests for future articles, please let them be known through the comments.
cheers, and happy hacking!
2 comments:
Good stuff - please continue the series!
People should read this.
Post a Comment