I've read a few articles lately (like this one), advocating the use for the method_missing method in Ruby.
Many people seem to have a passionate love affair with `method_missing`, but aren't very careful in how they handle their relationship. So, I'd like to address the question:
How should I use method_missing?
If you don't want to read through the reasons you should stay with your companion, `define_method`, and are just looking to rationalize your love-affair with `method_missing`, you can skip directly to when to use method_missing.
First of all, never give in to your love affair with `method_missing` without taking a moment to realize how good you have it. You see, in your day-to-day life, you rarely need `method_missing` as bad as you think you do.
Case: I need to allow one class to use the methods of another class.
This is the most common use-case I've seen for `method_missing`. It's especially popular in gems and Rails plugins. The pattern goes something like this:
class A
def hi
puts "Hi from #{self.class}"
end
end
class B
def initialize
@b = A.new
end
def method_missing(method_name, *args, &block)
@b.send(method_name, *args, &block)
end
end
A.new.hi #=> Hi from A
B.new.hi #=> Hi from A
So now B has all of A's instance methods. But let's think about what happens every time we call `@b.hi`, your ruby runtime goes up the chain to find the method, and then at the very end, it calls method_missing right before it raises a NoMethodError.
In the example above, this isn't a big deal, as there are only 2 trivial classes to look through. But often, we're programming in the context of Rails or some other framework. And your Rails model inherits from ActiveRecord, which in-turn inherits from an ass-load of other classes, and now you have quite a stack to climb... every time that method is called!
Now you're saying, "But Steve, I need method_missing." I say to you, don't forget you have a faithful companion in `define_method`.
It allows you to dynamically define a method (go figure!). The great thing about `define_method` is that after it runs (typically when your classes are loaded), the methods it created now exist for your class, plain and simple. No climbing up the class chain when you call those created methods.
`define_method` is loving and dependable, and will more than satisfy in your day-to-day life. Don't believe me? Read on...
class B
define_method(:hi) do
@b.hi
end
end
"But I have lot's of methods!" you say.
"No problem!" I respond with a reassuring wink.
class B
[:hi, :bye, :achoo, :gesundheit].each do |name|
define_method(name) do
@b.send(name)
end
end
end
But I don't want to have to name them all explicitly.
Now you're getting picky, but OK.
class A
# ... lots of methods in here
end
class B
A.instance_methods.each do |name|
define_method(name) do
@b.send(name)
end
end
end
But what if I need to build in new, slightly modified methods that are just based on others?
That's easy.
class A
def hi
puts "Hi."
end
end
class B
A.instance_methods.each do |name|
define_method("what_is_#{name}") do
if @b.respond_to?(name)
@b.send(name)
else
false
end
end
end
end
B.new.what_is_hi #=> "Hi."
B.new.what_is_wtf #=> false
That's not very attractive.
Yeah well, you take what you can get. If you want more readable code, check out ruby delegation library and Rails ActiveRecord delegation.
So, let's put this all together to see the real power of `define_method`.
Adapted from this example on ruby-doc.org:
class A
def fred
puts "In Fred"
end
def create_method(name, &block)
self.class.send(:define_method, name, &block)
end
define_method(:wilma) { puts "Charge it!" }
end
class B < A
define_method(:barney, instance_method(:fred))
end
a = B.new
a.barney #=> In Fred
a.wilma #=> Charge it!
a.create_method(:betty) { p self.to_s }
a.betty #=> B
By now you're thinking, well there's got to be a good reason to use it, or it wouldn't exist. And you're right, there is.
Case: I want to provide a set of methods, based on some pattern, that do something as a function of the method name itself. I probably will never call most of the possible methods, but they need to be available.
Now you're talking! This is actually how ActiveRecord provides you with all those fancy dynamic attribute-based finders, like `find_by_login_and_email(user_login, user_email)`.
def method_missing(method_id, *arguments, &block)
if match = DynamicFinderMatch.match(method_id)
attribute_names = match.attribute_names
super unless all_attributes_exists?(attribute_names)
if match.finder?
# ...you get the point
end # my OCD makes me unable to omit this
# ...
else
super # this is important, I'll tell you why in a second
end
end
`method_missing` is the perfect tradeoff when you have a large proportion of available meta-methods compared to the amount that will actually be used.
Think about the attribute-based finders in ActiveRecord. In order to define those methods up front with `define_method`, ActiveRecord would have to introspect the columns available for each model's table, and in each model build methods for every conceivable combination of columns.
find_by_email
find_by_login
find_by_name
find_by_id
find_by_email_and_login
find_by_email_and_login_and_name
find_by_email_and_name
# ...
You get the point. If your table has 10 columns, that's 10-factorial possible combinations! $latex 10! = 10 \times 9 \times 8 \times \ldots = 3,628,800$ So, you would have to define 3,628,800 methods at run-time, and then ruby would have to keep those methods in memory.
All I'm saying is, if you think you can perform with this kind of overhead, you better be Tiger Woods.
It's normal at this point in your life to feel attracted to `method_missing`. Even though you're committed to `define_method`, there are times when you just need to be with `method_missing`. If you're going to do it, please just be safe.
As long as you practice safe-programming, you can use `method_missing` worry-free. I'll leave you with four safe-programming guidelines to follow:
Be responsible and know what your code is doing and who it's associating with. You don't want to be running code arbitrarily every time you call some method that doesn't exist. So, before you run your code inside `method_missing`, test the method call. Maybe something like this:
def method_missing(method_id, *arguments, &block)
if method_id.to_s =~ /^what_is_[\w]+/
# do your thing
end
end
When you're inside `method_missing`, after you've tested the called method and determined it does indeed match your criteria to run your special code, wrap your code in a `define_method` block:
def method_missing(method_id, *arguments, &block)
if method_id.to_s =~ /^what_is_[\w]+/
self.class.send :define_method, method_id do
# do your thing
end
self.send(method_id)
end
end
Now, the first time that method is called, it runs up the class chain looking for the method. It finally hits `method_missing`, passes your test, and then `define_method` is called and the code runs. Now every subsequent call to that specific method will be defined, and it won't have to run up the entire chain.
ActiveRecord actually does this, and it's allows a considerable performance boost. Especially with Rails, where you pick one of these finders to use in your model or controller, and it then gets called over and over again.
If anything goes wrong, you need to grab your stuff and get out of there. And ideally, you don't want to leave a trace. If the called method fails your criteria, at any point, call super. This will allow the method call to continue on it's merry way up the class chain, so that it can raise the appropriate exception and leave you an exception stack trace you can work with.
def method_missing(method_id, *arguments, &block)
if method_id.to_s =~ /^what_is_[\w]+/
self.class.send :define_method, method_id do
# do your thing
end
self.send(method_id)
else
super
end
end
Wait, what? Ok, so the analogy kinda breaks down here, but at least in the programming world, you want to broadcast to your application that your class now responds to the method you made.
def method_missing(method_id, *arguments, &block)
if method_id.to_s =~ /^what_is_[\w]+/
self.class.send :define_method, method_id do
# do your thing
end
self.send(method_id)
else
super
end
end
def respond_to?(method_id, include_private = false)
if method_id.to_s =~ /^what_is_[\w]+/
true
else
super
end
end
Now, any other gems or libraries that rely on querying your class for available methods will still work with your new meta-methods. This code is a bit redundant, so check out this writeup on TechnicalPickles, which describes how ActiveRecord extracts this redundancy out. Hat-tip to Aditya Sanghi for pointing out this good practice on the #rails-contrib IRC.
The lesson: These methods play an important role in every rubyist's life. Treat `define_method` as your faithful companion, `method_missing` as your passionate but careful mistress, and `respond_to?` as your love-child, and you can't go wrong.
Comments are loading...