Dynamic Methods vs. Method Missing

Posted by Paul McMahon 08/09/2011 at 09h38

For a class I'm organizing on metaprograming in Ruby, we’re using Metaprograming Ruby as a textbook. In the chapter Tuesday: Methods, the book introduces two techniques for removing duplication: dynamically defining methods and using method_missing. It gives examples of applying both techniques to some sample code of which a simplified version is reproduced below:

# initial code
class Computer
  def initialize(data_source)
    @data_source = data_source
  end

  def mouse
    puts "Price: #{data_source.mouse_price}"
  end

  def keyboard
    puts "Price: #{data_source.mouse_price}"
  end

  def monitor
    puts "Price: #{data_source.mouse_price}"
  end
  
  #...
end

# dynamic methods
class Computer
  def initialize(data_source)
    @data_source.methods.grep(/^(.*)_price$/) { Computer.define_component $1 }
  end
  
  def self.define_component(name)
    define_method(name) do
      puts "Price: #{data_source.send("#{name}_price")
    end
  end
end

# method_missing
class Computer
  instance_methods.each do |m|
    undef_method m unless m.to_s =~ /^__|method_missing|respond_to?/
  end

  def method_missing(name, *args)
    super if !respond_to?(name)
    puts "Price: #{data_source.send("#{name}_price")
  end

  def respond_to?(method)
    @data_source.respond_to?("#{name}_price")
  end
end

However, the book doesn't discuss when you should use one technique versus another. Here’s my rule: only use method_missing when it is infeasible to use dynamic methods.

One good example of using method_missing is ActiveRecord's find_by method. With it, you can call a method like find_by_name_and_birthdate. While ActiveRecord could theoretically use the dynamic method technique, generating every possible method would be overkill, thus using method_missing makes sense.

One counter example can be found in an example in the Metaprograming Ruby book itself:

class Roulette
  def method_missing(name, *args)
    person = name.to_s.capitalize 
    super unless %w[Bob Frank Bill].include? person
    number = 0
    3.times do
      number = rand(10) + 1
      puts "#{number}..."
    end
    "#{person} got a #{number}"
  end
end

In this case, we have three methods we want Roulette to respond to: bob, frank, and bill. As we know these methods in advance, you should use the dynamic_method strategy:

class Roulette
  %w[bob frank bill].each do |name|
    define_method name do
      number = 0
      3.times do
        number = rand(10) + 1
        puts "#{number}..."
      end
      "#{name.to_s.capitalize} got a #{number}"
    end
  end
end

Now back to our initial example with Computer, which technique should we apply? Given that we can apply the dynamic methods technique, we should use it. Oh, and assuming we are always passed an instance of DataStore, I'd refactor it like so:

# dynamic methods
class Computer
  DataSource.methods.grep(/^(.*)_price$/).each do |name|
    define_method(name) do
      puts "Price: #{data_source.send("#{name}_price")
    end
  end
end