CSC/ECE 517 Fall 2007/wiki1b 2 Method Missing

From Expertiza_Wiki
Jump to navigation Jump to search

A world without "Method Missing"

Imagine an object calling a method that does not exist. It will throw an exception which definitely is undesirable. Now, if there could be a method that would perform a useful operation during such occurences then, the program can be made more useful.

What is "Method Missing" and what the fuss is all about ???

Ruby has a special feature that takes care of the situation when a call to an undefined method is made on an object, it provides an option to intercept the call. This is done by the method ‘method_missing’. In other words it is called whenever someone tries to call a method in your object that doesn't exist. Using this hook, we can "pretend" to accept method calls that aren't defined.

This is especially powerful when combined with object composition. Here's an example:

class SimpleCallLogger

 def initialize(o)
   @obj = o
 end
 def method_missing(methodname, *args)
   puts "called: #{methodname}(#{args})"
   a = @obj.send(methodname, *args)
   puts "\t-> returned: #{a}"
   return a
 end

end

This object "intercepts" all method calls made to it, prints out a message and forwards on the method call to an internal object using the 'send' method-- without knowing anything about the object passed to it.

method_missing is in part a safety net: It gives you a way to intercept unanswerable messages and handle them gracefully.


Provide Extensive APIs with Method Missing!

This section covers how we can use Method Missing to provide a very comprehensive list of API methods in a class, with a little amount of code.

Lets assume that we are creating a flickr search API class. What could be the interface of this class? Well basically, the purpose of this class would be to provide a variety of methods using which developers can search photos in flickr database. For example, assume that our photo database has attributes like keywords, tags, user, content_type, free_photos and date for each photo. And we want to define the following methods in the Flickr Search API:

flickr.search_by_keywords("Wolfpack")
flickr.search_by_tags("Sports")
flickr.search_by_user("Vinay")
flickr.search_by_content_type("paintings")
flickr.search_by_free_photos("Million dollar Art")
flickr.search_by_date_added_on("10/1/2007")

A normal approach to this would be to define 6 different methods in our code and let the sub classes use these 6 methods to access the search API. Now, lets see the downside of this approach. Suppose after a year, we decide to introduce new features to flickr API like tagging each photo by its location. To achieve this, a new attribute called "location" can be added to our database which would tell where that particular photo was taken. Now, to provide the functionality of "search by location" we will have to modify our Search API class, so that sub classes can use this new search method. This means time and $$$...

Now, lets do it in a Ruby way! This can be easily achieved by Method Missing. Have a look at the following code and the inlined comments:

def method_missing(method_id, *arguments)
  # Enter only if method called is of the type "search_by"
  if match = /search_(by)_([_a-zA-Z]w*)/.match(method_id.to_s)
  
    # Extract the attribute names, for ex. tags, keywords etc
    attribute_names = extract_attribute_names_from_match(match)

    # Check if the attributes exist in the database, else call Method_missing method of Superclass
    super unless all_attributes_exists?(attribute_names)

    # Construct conditions from the attribute names and arguments
    # For different pair of attribute and argument, different conditions can be constructed.
    # For example, take a pair (keywords,Wolfpack), for which conditions like "keywords = 'Wolfpack'" or
    # "keywords LIKE '%Wolfpack%'" can be created.
    conditions = construct_conditions_from_arguments(attribute_names, arguments)
    
    # Create an array of conditions so that it can be passed around methods
    options = { :conditions => conditions }
    
    # Execute the database query here and return the results
  else
    # If the called method is not of the type "search_by", call the method_missing of Parent Class
    super
  end
end

The above code works in a simple way. Method_missing will be called whenever the called method instance is not found anywhere. The code extracts the attribute name and the arguments and then create an options array containing the database query conditions. This condition array is then use to create database queries.

Now you can easily understand why Ruby implementation is far better! Now, if we plan to provide the functionality to search the photos by location, we just have to add a new attribute to our database, and thats all! No need to even touch the code!

Note: The above example was made simple purposefully to highlight method_missing feature of Ruby. Further reading can be done at @ http://ajax.stealthsettings.com/rubyisms-in-rails/metaprogramming/ which is the original source of this idea.

Build your own Domain Specific Language (DSL) with Ruby!

Another powerful application of Method Missing is that it can be used to create a DSL. You can define your own language as a class and the inheriting classes can use this DSL to code specific tasks faster!

Lets look at an example to understand this concept:

Lets define a DSL to create a recipe. For example to create a breakfast recipe you would only have to write

"italian breakfast".consists_of
                               .caffe(1 , :macchiato )
                               .cornetto(1, :cioccolato)

Above statement would create a recipe having items cafee and cornetto of the type macchiato and cioccolato. To display the list of recipe items, the following statement can be used:

recipe? "italian breakfast"    

and following will be the output :

    to make italian breakfast you should buy:
     * 1 cioccolato cornetto
     * 1 macchiato caffe

Now lets try to do this. Consider the following statement

"italian breakfast".consists_of.
                        caffe(1 , :macchiato ).
                        cornetto(1, :cioccolato)

Here a method "consist_of" is being called as a string method. Therefore, we need to define this method in String class (which can be done in Ruby!).So, we include the following code:

class String
  #Recipe conjuction
  def consists_of
    Recipe .new self
  end
end

The above method creates a new instance of class recipe and returns a reference to the created instance. Since, the reference is returned, we can use it to call methods in Class Recipe.

Now, its time to define class recipe:

 class Recipe
    # Class variable to store all the recipes
    Public:
    @@recipes = {}

  def initialize recipe_name
    @name = recipe_name
    @ingredients = {}
    @@recipies[@name] = self
  end

  # Method missing will take the ingredients provided and put it in a hash table 
  def method_missing method, *args
    @ingredients[method .to_s] = args

    # Returning self(reference to the class instance) will enable chaining of multiple ingredients.
    # Now we can add ingredients by <class_instance>.<ingredient1>.<ingredient2>.<ingredient3> ...
    return self
  end

Now, using the above code we can create any recipe. And next is to write the code which would print the following output on calling the method recipe? "italian breakfast"

to make italian breakfast you should buy:
  * 1 cioccolato cornetto
  * 1 macchiato caffe
class Recipe
  …
  def report
    puts "to make #{@name} you should buy:" 
    @ingredients.each_pair do
      |ingredient,description|
      puts " * #{Array(description).join ' '} #{ingredient}"
    end
  end
  … 
end

Now we need a method to link recipe? to report method of class Recipe.

def recipe? name
  Recipe.recipes[name].report
end

and its done!

You can now do:
    "italian breakfast".consists_of.
                        caffe(1 , :macchiato ).
                        cornetto(1, :cioccolato)

    recipe? "italian breakfast"    

and get:
    to make italian breakfast you should buy:
     * 1 cioccolato cornetto
     * 1 macchiato caffe

Note: The above example was taken from the original source http://liquiddevelopment.blogspot.com/2006/04/twisting-and-shaping-dsls-using-ruby.html.

"The Good" about Method Missing!

>> XMLRPC::Client::Proxy will pass its method calls directly onto the service. You can even specify a prefix, which makes the whole thing a little more psuedo-OO.

>> Builder::XmlBase translates method calls into XML tags.

>> Lafcadio, an object-relational mapping layer for Ruby and MySQL uses method_missing to dynamically define getters and setters for domain objects.

>> Hash with Attrs makes your Hashes act like JavaScript objects.

class Hash
  def method_missing(meth,*args)
    if /=$/=~(meth=meth.id2name) then
      self[meth[0...-1]] = (args.length<2 ? args[0] : args)
    else
      self[meth]
    end
  end
end

"The Bad" about Method Missing!

Method Missing is surely a quick way of getting things done but there is a price to pay in the form of slower execution time. The following text describes about what happens when a unknown method is called and the source of this information is http://www.thirdbit.net/articles/2007/08/01/10-things-you-should-know-about-method_missing/ .

"When you send a message to a Ruby object, Ruby looks for a method to invoke with the same name as the message you sent. (There are a bunch of different ways to send the message, but the most common one is just obj.method_name. But you can make the fact that you are sending a message explicit by using obj.send(:method_name)). First it looks in the current self object’s own instance methods. Then it looks in the list of instance methods that all objects of that class share, and then in each of the included modules of that class, in reverse order of inclusion. Then it looks in that class’s superclass, and then in the superclass’s included modules, all the way up until it reaches the class Object. If it still can’t find a method, the very last place it looks is in the Kernel module, included in the class Object. And there, if it comes up short, it calls method_missing. You can override method_missing anywhere along that method lookup path, and tell Ruby what to do when it can’t find a method."

It doesn't mean that we should not use Method missing! Normally the slowness factor is not that big to make an impact on the overall application. But again, before using method missing we should carefully analyze issues like design, speed, maintainability and extendibility of code.

"The Ugly" about Method Missing!

There is nothing ugly about Ruby! :)

References

http://rubylearning.com/satishtalim/ruby_method_missing.html
http://ajax.stealthsettings.com/rubyisms-in-rails/metaprogramming/
http://liquiddevelopment.blogspot.com/2006/04/twisting-and-shaping-dsls-using-ruby.html
http://riffraff.blogsome.com/2006/05/02/metaprogramming-breakfast/
http://www.thirdbit.net/articles/2007/08/01/10-things-you-should-know-about-method_missing/

Other Cool Links to Ruby Lovers!

http://poignantguide.net/ruby/ -> A Funny way to Learn Ruby
http://redhanded.hobix.com/ -> Interesting hacks and code snippets
http://www.rubyinside.com/ -> Stay tuned with whats new in Ruby
http://www.rubyonrails.org/ -> The Best Ruby Framework!