Why method_missing exists
Normally, an object only responds to messages that match the names of the object’s methods and public accessors. For example, if I send the message first_name
to an instance of User
, then the User
object will only respond to my message of first_name
if User
has a method or accessor called first_name
.
But sometimes it’s useful to allow objects to respond to messages that don’t correspond to methods or accessors.
For example, let’s say we want to connect our User
object to a database table. It would be very convenient if we could send messages to instances of User
that correspond to the database table’s column names without having to either explicitly define new methods or do something inelegant like, for example, user.value(:first_name)
. It would be better if we could get the database value by calling user.first_name
.
What’s more, if we added a new column called last_name
, it would be good if we could just do user.last_name
without having to change any code.
method_missing
allows us to do things like this. In this post we’ll see how by going through an example that’s similar to (but simpler than) the database example above.
Arbitrary attribute setter example
In the below example, we’ll use method_missing
to define some behavior that allows us to arbitrarily set values on an object. We’ll have an object called user
on which we can call set_first_name
, set_last_name
, set_height_in_millimeters
or whatever other arbitrary values we want.
A plain User object
In the following snippet, we define a class called User
which is completely empty. We attempt to call set_first_name
on a User
instance which, of course, fails because User
has no method called set_first_name
.
# user.rb
class User
end
user = User.new
user.set_first_name("Jason")
When we run the above, we get undefined method `set_first_name' for #<User:0x00000001520e01c0> (NoMethodError)
.
$ ruby user.rb
Traceback (most recent call last):
user.rb:9:in `<main>': undefined method `set_first_name' for #<User:0x00000001520e01c0> (NoMethodError)
Adding method_missing
Now we add a method to the User
class with a special name: method_missing
.
In order for our method_missing
implementation to work it has to follow a certain function signature. The first parameter, method_name
, corresponds to the name of the message that was passed (e.g. first_name
). The second parameter, *args
corresponds to any arguments that were passed, and comes through as an array, thanks to the splat operator.
In this snippet all we’ll do is output the values of method_name
and *args
so we can begin to get a feel for how method_missing
works.
class User
def method_missing(method_name, *args)
puts method_name
puts args
end
end
user = User.new
user.set_first_name("Jason")
When we run this we see set_first_name
as the value for method_name
and Jason
as the value for args
.
$ ruby user.rb
set_first_name
Jason
Parsing the attribute name
Now let’s parse the attribute name. When we pass set_first_name
, for example, we want to parse the attribute name of first_name
. This can be done by grabbing a substring that excludes the first four characters of the method name.
class User
def method_missing(method_name, value)
attr_name = method_name.to_s[4..]
puts attr_name
end
end
user = User.new
user.set_first_name("Jason")
This indeed gives us just first_name
.
$ ruby user.rb
first_name
Setting the attribute
Now let’s set the actual attribute. Remember that args
will come through as an array. (The reason that args
is an array is because whatever method is called might be passed multiple arguments, not just one argument like we’re doing in this example.) We’re interested only in the first element of args
because user.set_first_name("Jason")
only passes one argument.
class User
def method_missing(method_name, *args)
attr_name = method_name.to_s[4..]
instance_variable_set("@#{attr_name}", args[0])
end
end
user = User.new
user.set_first_name("Jason")
puts user.instance_variable_get("@first_name")
When we run this it gives us the value we passed it, Jason
.
$ ruby user.rb
Jason
We can also set and get any other attributes we want.
class User
def method_missing(method_name, *args)
attr_name = method_name.to_s[4..]
instance_variable_set("@#{attr_name}", args[0])
end
end
user = User.new
user.set_first_name("Jason")
user.set_last_name("Swett")
puts user.instance_variable_get("@first_name")
puts user.instance_variable_get("@last_name")
When we run this we can see that both values have been set.
$ ruby user.rb
Jason
Swett
A note about blocks
In other examples you may see the method signature of method_missing
shown like this:
def method_missing(method_name, *args, &block)
method_missing
can take a block as an argument, but actually, so can any Ruby method. I chose not to cover blocks in this post because the way method_missing
‘s blocks work is the same as the way blocks work in any other method, and a block example might confuse beginners. If you’d like to understand blocks more in-depth, I’d recommend my other post about blocks.
Takeaways
method_missing
can be useful for constructing DSLs.method_missing
can be added to any object to endow that object with special behavior when the object gets sent a message for which it doesn’t have a method defined.method_missing
takes the name of the method that was called, an arbitrary number of arguments, and (optionally) a block.
Thanks Jason!
Once again you expanded my brain!
Thanks!
Thank you!
Needed this. I was going to use .Try, but this was awesome to learn. Many thanks!
Thanks Jason, you just demistified method_missing for me
Sorry, but this isn’t a good approach.
0. Always implement respond_to_missing? when implementing method_missing?.
1. Always defer to super whenever a case isn’t handled.
For example, when proxying a contained object:
def respond_to_missing?(name, include_all)
proxy.respond_to_missing?(name, include_all) || super
end
def method_missing?(name, *args, &block)
if proxy.respond_to?(name)
proxy.send(name, *args, &block)
else
super
end
end