How and why to use Ruby’s method_missing

by Jason Swett,

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.

7 thoughts on “How and why to use Ruby’s method_missing

  1. Anon

    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

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *