Understanding method_missing in Ruby

 ruby    metaprogramming  

Overview

In this post we will review how to use the method_missing method in Ruby together with some guidelines and best practices.

Do not use method_missing as the first option of solving a particular problem

First of all, it is not advised to overwrite the private method BasicObject#method_missing as the first option of solving a particular problem.

Like all metaprogramming solutions, use this technique only if it saves time or if you failed to come up with a simpler way to solve the problem.

Simple is better than complex. This is one of the main reasons this technique is not frequently applied in practice. If not properly implemented, we may end up in an infinite recursion.

So, what is method_missing used for?

It gives us one last possibility to cope with the missing method prior to raising the well known NoMethodError exception.

When a given object obj receives a nonexisting method call (message) (e.g. obj.hello), the interpreter starts crawling the ancestors chain in an attempt to find a suitable method to invoke. If Ruby fails to find a suitable method it raises NoMethodError.

We can modify this behaviour by overwriting BasicObject#method_missing somewhere in the ancestors chain and handle the missing method definitions in a custom way.

Introspection and method_missing

The intentions of using this technique is to treat (some of) the missing methods as ones that are already defined (e.g. having a certain method name pattern or some other rule).

Therefore, we need to also overwrite the BasicObject#respond_to_missing? method so introspection (e.g. with respond_to?, method, etc.) works as expected.

Example using method_missing

DISCLAIMER: This is a code-smell and one should consider using a simpler approach to such solutions.

Imagine that you want to have your Company class to handle all methods starting with check_ in such a way that a check to an external service is made.

  company_number = '1234567890'
  my_company = Company.new(company_number)

  my_company.check_company_name
  my_company.check_address
  my_company.check_legal_form
  my_company.check_owners

  # =>
  # call an external service to get the data
  # for the methods prefixed with `check_`
  

Here is a simple example implementation of the Company class:

  class Company
    attr_reader :company_number

    def initialize(company_number)
      @company_number = company_number
    end

    def method_missing(method_name, *args, &block)
      if method_name.to_s.split('_')[0] == 'check'
        puts "[#{company_number}][#{method_name}] Connecting to the server.."
        # TODO: return the result of the check here
      else
        super
      end
    end

    def respond_to_missing?(method_name, *args, &block)
      method_name.to_s.split('_')[0] == 'check' or super
    end
  end
  

...and now some method invocations:

  my_company = Company.new('1234567890')
  puts my_company.respond_to?(:check_company_name) # => true

  my_company.check_company_name
  # => [1234567890][check_company_name] Connecting to the server..

  check_legal_form = my_company.method(:check_legal_form)
  check_legal_form.call
  # => [1234567890][check_legal_form] Connecting to the server..
  

Conclusion

References