logo
  • Jobs
  • About Me
  • Contact
  • Home
« Testing Helper Modules with Rails 2.2
puts vs print in ruby »

Implementing method_missing

Posted February 10th, 2009 by Matt Berther

Earlier, I was working on creating a script that would iterate a set of folders and execute a chunk of code against that file when it was found. The script itself is easy enough to write with straight ruby. For example:

Dir.glob("./**/*").each do |f|
  # your ruby code here
end

However, as much as we tend to do this during our daily life, I was hoping to hide this behind an api that some less technical users may be able to use. To promote reuse, I created a class that would provide this functionality.

class FileWalker
  def root_path=(path)
    @root_path = path
  end
 
  def each_file_of_type(type, &block)
    Dir.glob("#{@root_path}/**/#{type}").each do |f|
      yield f
    end
  end
end

The users of this API utilized it in a manner which you would expect (coming from a statically-typed language):

walker = FileWalker.new
walker.root_path = "."
walker.each_file_of_type("*.rb") do |file|
  # your ruby code here
end

While this covered the functional requirements of what I was hoping to do, I really did not like the way this reads. One, the user of the API was expected to implement the wildcard for the type. Two, did I say that I didnt like the way it reads? What I was really hoping for was an API that worked like this:

FileWalker.dir "." do
  each_rb_file do |f|
    # your ruby code here
  end
end

The first thing you’ll notice is that I remove the file specification from the call with the each_rb_file method. However, I certainly do not want to corrupt the FileWalker class with dozens of methods to iterate different file types. Having to add a new method every time I want to iterate a different file type would most certainly violate the open/closed principle.

As I thought about the best way to accomplish both the API I desired and the long-term maintenance, I decided to take advantage of a great method on ruby’s Object class called method_missing. method_missing is a method that gets called every time a method on the receiver does not exist. The method gets passed three arguments, the name of the method, any arguments to the method, and a block to execute.

Using a little regexp magic, I am able to intercept calls to each_rb_file and delegate them to the earlier each_file_of_type method (which is now private). Take a look:

class FileWalker
  def initialize(path)
    @root_path = path
  end
 
  def self.dir(path, &block)
    walker = new(path)
    walker.instance_eval(&block) if block_given?
  end
 
  def method_missing(method, *args, &block)
    if method.to_s =~ /^each_(.+)_file/
      each_file_of_type "*.#{Regexp.last_match[1]}", &block
    end
  end
 
private
  def each_file_of_type(type, &block)
    Dir.glob("#{@root_path}/**/#{type}").each do |f|
      yield f
    end
  end
end

So, as you can see with a little method_missing mojo, we now have access to any number of methods that allow us to retrieve all files of a particular extension. The above script and class will work with any sort of file extension. Calling each_exe_file, each_xml_file, or each_py_file will all function the same way, without adding any new code. The advantage here, as mentioned earlier, are that 1) we get to provide a *readable* API to our consumers, and 2) we conform to the open/closed principle by not having to modify already written and tested code to implement a new extension.

If you’ve not looked much at method_missing and ruby, I encourage you to discover it more. I’ve only touched the surface with its capabilities. Others have gone much further, including implementing an entire XML dsl utilizing this method.

Perhaps in a subsequent post I’ll dive into how I’d go about implementing this… til then.

2 Comments

This entry was posted on Tuesday, February 10th, 2009 at 9:39 pm and is filed under Uncategorized. You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response, or trackback from your own site.

roger
February 20th, 2009

File.find might traverse it, as well [without loading the whole thing into RAM?]

geo
September 5th, 2009

Nice implementation.

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>
-->

flag
Favorite Charity
wounded warrior project
Search
Social
  • mattberther on twitter
  • mattberther on linkedin
Syndication
Archives
  • January 2010
  • September 2009
  • July 2009
  • June 2009
  • February 2009
  • January 2009
  • December 2008
  • November 2008
  • September 2008
  • August 2008
  • June 2008
  • May 2008
  • April 2008
  • March 2008
  • February 2008
  • January 2008
  • December 2007
  • November 2007
  • October 2007
  • September 2007
  • August 2007
  • July 2007
  • June 2007
  • May 2007
  • April 2007
  • March 2007
  • February 2007
  • January 2007
  • December 2006
  • November 2006
  • October 2006
  • September 2006
  • August 2006
  • July 2006
  • June 2006
  • May 2006
  • April 2006
  • March 2006
  • February 2006
  • January 2006
  • December 2005
  • November 2005
  • October 2005
  • September 2005
  • August 2005
  • July 2005
  • June 2005
  • May 2005
  • April 2005
  • March 2005
  • February 2005
  • January 2005
  • December 2004
  • November 2004
  • October 2004
  • September 2004
  • August 2004
  • July 2004
  • June 2004
  • May 2004
  • April 2004
  • March 2004
  • February 2004
  • January 2004
  • December 2003
  • November 2003
  • October 2003
  • September 2003
  • August 2003
  • July 2003
  • June 2003
  • May 2003
  • April 2003
  • March 2003
mattberther.com © 2003 - 2010