Make it up as you go
One way Ruby is dynamic is that you can choose how to handle methods that are called, but don’t actually exist. If you have a lot of very similar methods, you can even use this to define them all at once! Ruby does this using the method_missing method, which you override in the classes where you need more dynamic method calling.
ActiveRecord’s dynamic find_all_by methods
Ruby on Rails uses method_missing with ActiveRecord’s find_all_by methods. There is no find_all_by_name method, but if your Person model has a name attribute, you can call Person.find_all_by_name('Bob') and it will return all the records that match that name.
Here’s a very simplified version of how Rails handles find_all_by requests:
class Person < ActiveRecord::Base
def self.method_missing method_name, *args
if method_name =~ /^find_all_by_(\w+)$/
self.all(:conditions => {$1 => args[0]})
end
end
end
Using regular expressions, method_missing sees if the method name matches something we expect. It parses out the interesting parts, and uses them to look up the objects we’re searching for. This is a good use case, because the attributes of an ActiveRecord model aren’t known until runtime.
Dynamic methods for dynamic objects outside Rails
We can apply this same technique outside of Rails. Let’s create the world’s most dynamic Ruby class:
# lib/widget.rb
class Widget
def method_missing sym, *args
if sym =~ /^(\w+)=$/
instance_variable_set "@#{$1}", args[0]
else
instance_variable_get "@#{sym}"
end
end
end
We’ve just created a Widget object that can have any attributes you want to give it. method_missing checks if the called method ends with an equal sign – if so, it assigns the value you passed, to an instance variable with that name. If there’s no equal sign, it tries to get the value of an instance variable by that name:
ruby-1.9.2-p0 > widget = Widget.new => #<Widget:0x0000010383f618> ruby-1.9.2-p0 > widget.name = 'Bob' => "Bob" ruby-1.9.2-p0 > widget.age = 30 => 30 ruby-1.9.2-p0 > widget.name => "Bob" ruby-1.9.2-p0 > widget.age => 30
Use method_missing with methods that use blocks
You can also pass blocks to method_missing. Say we have an ActiveRecord model called Person, with name and age attributes. Let’s create something similar to find_all_by that gets the list of matching people, and runs them through the map method. We’ll call it map_by:
# app/models/person.rb
class Person < ActiveRecord::Base
def self.method_missing method_name, *args, &block
if method_name =~ /^map_by_(\w+)$/
list = self.all(:conditions => {$1 => args[0]})
list.map(&block)
end
end
end
If a method is called that can’t be found, method_missing will check to see if it matches our map_by pattern, perform an ActiveRecord search, and push the results through map with the block we supplied.
Now let’s see if it works, by grabbing the names of all people in our database age 30:
ruby-1.9.2-p0 > Person.create :name => 'Bob', :age => 30
=> #<Person id: 2, name: "Bob", age: 30, created_at: "2010-12-21 02:23:57", updated_at: "2010-12-21 02:23:57">
ruby-1.9.2-p0 > Person.create :name => 'John', :age => 29
=> #<Person id: 3, name: "John", age: 29, created_at: "2010-12-21 02:24:11", updated_at: "2010-12-21 02:24:11">
ruby-1.9.2-p0 > Person.create :name => 'Marsha', :age => 30
=> #<Person id: 4, name: "Marsha", age: 30, created_at: "2010-12-21 02:24:22", updated_at: "2010-12-21 02:24:22">
ruby-1.9.2-p0 > Person.map_by_age(30){|person| person.name}
=> ["Bob", "Marsha"]
It works! Now I’m going to refactor the Person class to make it easier to add more dynamic methods in the future. I’ll even add an each_by handler so we can see multiple dynamic methods in action:
# app/models/person.rb
class Person < ActiveRecord::Base
class << self
def method_missing method_name, *args, &block
case method_name
when /^map_by_(\w+)$/ then map_by $1, args[0], &block
when /^each_by(\w+)$/ then each_by $1, args[0], &block
else super method_name, *args, &block
end
end
def map_by attribute, value, &block
list = self.all(:conditions => {attribute => value})
list.map(&block)
end
def each_by attribute, value, &block
list = self.all(:conditions => {attribute => value})
list.each(&block)
end
end
end
I’ve done a few things. First, I changed our “if” conditional to a case statement, so that we can add to it in the future, and it will be clean and readable. I also moved the actual map_by code into its own method, for the same reason. And now, method_missing calls its parent method if it doesn’t find a match, to preserve inheritance.
You might also notice that instead of defining self.method_missing and self.map_by, I’ve wrapped these method definitions in a class << self block that essentially does the same thing. I think this is cleaner when you have several class methods.
method_missing can be used in any Ruby class, so long as you can anticipate dynamic methods that the users of your class might need, and preserve the chain of inheritance. This should be used sparingly, when you can cut down on method definitions by defining them dynamically. It’s easy to abuse this, and there is extra overhead involved. But for the right situations, method_missing can create shorter, more readable code.
Tags: dynamic, methods, method_missing, Rails, Ruby, ruby on rails
