Dynamic method handling with #method_missing and #respond_to_missing? in Ruby and Rails
Tags: ruby, rails, metaprogramming, beginners
Published: 2019-08-09 06:21:55

I just finished reading "[The Well-Grounded Rubyist](https://www.manning.com/books/the-well-grounded-rubyist-third-edition)" and finally learned about the mysterious `#method_missing` method.

`#method_missing` gets called whenever an object receives a method call that it doesn't know how to handle. Maybe it was never defined, misspelled, or for some reason, ruby can't find it on the object's method look-up path.

The cool thing is is that you can use `#method_missing` to catch certain methods that you want to dynamically handle. Here's an example borrowed from "[The Well-Grounded Rubyist](https://www.manning.com/books/the-well-grounded-rubyist-third-edition)":

```ruby
class Student
  def method_missing(method, *args)
    if method.to_s.start_with?("grade_for_")
      puts "You got an A in #{method.to_s.split("_").last.capitalize}!"
    else
      super
    end
  end
end
```

This code would allow you to dynamically any method like `grade_for_english`, `grade_for_science`, etc. called on Student objects without needing to explicitly define several similar methods for each subject.

The name of the method that was called is always provided as a symbol through the `method` argument, so you can use it accordingly within your redefined version. The `*args` parameter grabs up any of the arguments that were passed along. It can also take an optional block, but I won't get into that here.

Make sure you always include `super` at the end of the redefined `#method_missing` so that any methods that cannot actually be handled in the end get passed back to the original definition of `#method_missing`

Thinking about my personal projects, I realized that `#method_missing` would finally let me solve one of the problems that had been bugging me for a while, so I'll use that as a real-world albeit simple example to illustrate how I used `#method_missing` to refactor my code.

# Background & Original Code

I have a basic web app for a farmer that sells organic vegetables as a boxed set, kind of like a CSA but on a one-time basis instead of a seasonal one. Anyway, the site is available in both English and Japanese so the data for objects needs to be stored in both languages. In this case, I have a class called `VeggieBox` that keeps track of the seasonal veggie boxes.  

I set up the app so that it pulls out the data from the database depending on the page's locale (`:en` or `:ja`). This enables me to share a single view file between the languages instead of separate views for each language.

The `VeggieBox` class makes objects with a title and description. To handle the different languages, the attributes are stored as `title_ja`, `title_en`, `description_ja`, and `description_en`.

Let's create a sample box now:

```ruby
@veggie_box = VeggieBox.new
@veggie_box.title_en = "Summer box"
@veggie_box.title_ja = "夏のボックス"
@veggie_box.description_en = "Tons of delicious summer veggies!!"
@veggie_box.description_ja = "美味しい夏野菜いっぱい！！"
```

Because the view file is shared, I had to figure out how to display the correct data depending on whether the user was looking at the English or the Japanese site. To do so, I originally created `#localized_title` and `#localized_description` that used `#send` to parse in the I18n locale and call the appropriate method:

```ruby
# app/models/veggie_box.rb

class VeggieBox < ApplicationRecord
  # ... other methods

  def localized_title
    self.send("title_#{I18n.locale}")
  end

  def localized_description
    self.send("description_#{I18n.locale}")
  end
end
```

Depending on which page the user views, the locale is set to either `en` or `ja`. So, if the user is on the English page, `@veggie_box.localized_title` would be equivalent to saying `@veggie_box.title_en` and pulling out the correct text in the view is as simple as:

```erb
<div class="description">
  <h3><%= @veggie_box.localized_title %></h3>
  <p><%= @veggie_box.localized_description %></p>
</div>
```

It's a fairly simple solution, but not as flexible as it could be. What happens if I started adding more and more attributes to the `VeggieBox` class? I would have to add a new `localize_***` instance method for every single attribute!

That's where `#method_missing` comes in to save the day.

# Refactor 1

Instead of having to use methods that have `localized_` attached to it, I decided to simplify it to the base attribute name so that I could just write `@veggie_box.title` or `@veggie_box.description` instead.

My first attempt to use `#method_missing` was something like this:

```ruby
# app/models/veggie_box.rb

class VeggieBox < ApplicationRecord
  # ... other methods

  def method_missing(method, *args)
    if method == "description" || "title"
      localized_method = "#{method}_#{I18n.locale}"
      self.send(localized_method)
    else
      super
    end
  end
end
```

If the app were to encounter something like `@veggie_box.title`, it wouldn't immediately know what to do because the VeggieBox object is calling a method that isn't explicitly defined; in this case `title`. It gets redirected to the `#method_missing` method defined in the `VeggieBox` class, which catches the unknown method `title` in the if-clause `if method == "description" || "title"`. Instead of throwing a NoMethodError, the call to `title` gets processed into `title_en` if the user was looking at the English page, or `title_ja` if it was the Japanese page.

We can try it out in the console:

```ruby
I18n.locale = :en
@veggie_box.title
 => "Summer box"

I18n.locale = :ja
@veggie_box.title
 => "夏のボックス"
```

Slightly better, but the method names are still hard-coded into the `if`-clause!

My next refactor will get rid of the hard-coding in the `#method-missing` definition.


# Refactor 2

To get rid of the hard-coded values, I decided to transform the method name into the localized attribute method (e.g. `title_ja`) by adding on the locale and then simply checking whether or not the veggie box object responds to that method. If so, the localized method is called on the object with send. If not, it gets sent to `super`.

Here is the refactored code:

```ruby
def method_missing(method, *args)
  localized_method = "#{method}_#{I18n.locale}"

  if self.respond_to?(localized_method)
    self.send(localized_method)
  else
    super
  end
end
```

Now, if another locale-based attribute were to be added to the `VeggieBox` class, `#method_missing` will be able to handle it without editing the code.

There is one slight problem that comes up when using `#method_missing` though, which I'll solve in the final refactor.

# Refactor 3

Although `#method_missing` will let your objects properly handle certain method calls dynamically, it doesn't allow them act like "real methods". That means ruby won't recognize them in other places, for example, when you use `object.respond_to?`:

```ruby
@veggie_box.respond_to?(:title)
 => false

@veggie_box.respond_to?(:description)
 => false
```

`respond_to?` returns `false` for the methods handled by `#method_missing`! Well, that's not right because it technically does respond to these methods even though they aren't defined.

Things like `object.method`, which let you turn methods into objects, won't work either:

```ruby
@veggie_box.method(:title)
=> NameError (undefined method `title' for class `#<Class:0x00007fc56289b2a8>')
```

I realized this after checking the [Ruby Style Guide's section on metaprogramming](https://rubystyle.guide/#no-method-missing) and [this great article](http://blog.marc-andre.ca/2010/11/15/methodmissing-politely/). They recommended also defining `#respond_to_missing?` when using `#method_missing`, which allows ruby to properly recognize that your objects do indeed respond to the meta-programmed methods. (Actually, the Ruby Style Guide recommends *not* using `#method_missing` at all... oops).

Here's how I defined `#respond_to_missing?` in my case:

```ruby
def respond_to_missing?(method, *)
  localized_method = "#{method}_#{I18n.locale}"
  attribute_names.include?(localized_method) || super
end
```

Here, I used Rails' ActiveRecord method `#attribute_names` to make sure the attributes were registered to the class. If so, it passes the `respond_to` test.

Each object would now return `true` when asked if they respond to `#title` or `#description`.

```ruby
@veggie_box.respond_to?(:title)
=> true

@veggie_box.respond_to?(:description)
=> true
```

The methods can now also be objectified:

```ruby
method = @veggie_box.method(:title)

I18n.locale = :en
method.call
 => "Summer box"

I18n.locale = :ja
method.call
 => "夏のボックス"
```

# Tests for Confidence

Considering this was the first time I used `#method_missing`, I wasn't 100% confident that the code would work correctly, so I added a test in the test suite (MiniTest) for the meta-programmed methods to give myself more confidence. Even if you are 100% confident, it would be a good idea to have a test anyway :)

The `assert_respond_to` tests make sure that instances of veggie boxes actually respond to the meta-programmed methods. The subsequent `assert_equal` tests first set the locale to `:en` or `:ja` and make sure the meta-programmed methods return the same values as the original attribute methods.

```ruby
require 'test_helper'

class VeggieBoxTest < ActiveSupport::TestCase

  def setup
    # sets up a test object from a fixture file
    @veggie_box = veggie_boxes(:box_one)
  end

  # ...other tests...

  test "should respond correctly to localized methods" do
    # Make sure it actually responds to the meta-programmed methods
    assert_respond_to @veggie_box, :title
    assert_respond_to @veggie_box, :description
 
    # Make sure the method gets the correct values for each locale by 
    # comparing to the original attributes
    I18n.locale = :ja
    assert_equal @veggie_box.title_ja, @box.title
    assert_equal @veggie_box.description_ja, @box.description

    I18n.locale = :en
    assert_equal @veggie_box.title_en, @box.title
    assert_equal @veggie_box.description_en, @box.description
  end
end
```

Hope you enjoyed the article and if you haven't used `#method_missing` before, hopefully this will help!

# References
- "[The Well-Grounded Rubyist](https://www.manning.com/books/the-well-grounded-rubyist-third-edition)" by David A. Black and Joseph Leo III
- [Ruby Style Guide](https://rubystyle.guide/#no-method-missing)
- [Method_missing, Politely](http://blog.marc-andre.ca/2010/11/15/methodmissing-politely/) blog article
- [Ruby Documentation on method_missing](https://docs.ruby-lang.org/en/2.6.0/BasicObject.html#method-i-method_missing) (English)
- [Ruby Documentation on method_missing](https://docs.ruby-lang.org/ja/latest/method/BasicObject/i/method_missing.html) (Japanese - has a nice code example even if you can't read the explanation)