Skip to Content
Course content

18: method_missing and Dynamic Methods

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was reviewing a PR from a junior dev who was building a wrapper for a massive legacy SOAP API. The API had hundreds of different "get" methods—get_user_name, get_user_email, get_order_status, and so on. He had spent three days manually writing wrapper methods for every single one of them. It was an exhausting, repetitive slog, and the file was over two thousand lines of boilerplate. I told him to delete the whole file. He looked at me like I was insane, but that's when I introduced him to method_missing. We replaced those two thousand lines with about fifteen lines of dynamic code.

Catching the Uncatchable with method_missing

In Ruby, when you call a method on an object, Ruby searches the class and its ancestors for a definition. If it hits a dead end, it doesn't just give up immediately; it calls a special method called method_missing. By default, this is what triggers the NoMethodError you've seen a hundred times. But if you override method_missing in your own class, you can intercept those "failed" calls and decide what to do with them.

Let's look at how we fixed that API wrapper. Instead of defining every possible method, we can just catch the call, parse the method name, and send the request to the API dynamically:

class LegacyApiClient
  def method_missing(method_name, *args, &block)
    if method_name.to_s.start_with?("get_")
      field = method_name.to_s.sub("get_", "")
      puts "Fetching #{field} from the legacy API..."
      # Imagine a real API call happens here
      "Value for #{field}"
    else
      super
    end
  end
end

client = LegacyApiClient.new
puts client.get_user_name  # "Fetching user_name from the legacy API..."
puts client.get_order_id   # "Fetching order_id from the legacy API..."
# puts client.do_something_else # This still raises NoMethodError because of 'super'

That super call is non-negotiable. If you don't call super when your logic doesn't match the method name, you break the inheritance chain and swallow NoMethodError, which makes debugging a total nightmare. Trust me on this one.

Optimizing with define_method

While method_missing is powerful, it's slow. Every time you call a missing method, Ruby has to climb the entire inheritance tree before it gives up and hits your override. If you're calling these methods thousands of times in a loop, you'll feel the lag. This is where define_method comes in.

Instead of just handling the call and returning a value, you can use method_missing to define the method on the fly. The first time the method is called, it's slow; every time after that, it's a standard, fast method call.

class LegacyApiClient
  def method_missing(method_name, *args, &block)
    if method_name.to_s.start_with?("get_")
      # Define the method so we don't hit method_missing again
      self.class.define_method(method_name) do |*m_args|
        field = method_name.to_s.sub("get_", "")
        "Value for #{field}"
      end
      send(method_name, *args)
    else
      super
    end
  end
end

I generally prefer this "define-on-demand" pattern because it gives you the flexibility of dynamics with the performance of static methods. It's essentially a caching mechanism for your methods.

Staying Honest with respond_to_missing?

There is a catch. If you use method_missing, you're lying to the rest of the system. If someone calls client.respond_to?(:get_user_name), Ruby will return false because the method doesn't actually exist in the method table yet. This breaks a lot of Ruby idioms and libraries that check for capability before execution.

To fix this, you must always override respond_to_missing?. This is a companion method that tells Ruby, "I don't have this method defined, but I know how to handle it if you call it."

class LegacyApiClient
  # ... (previous method_missing logic) ...

  def respond_to_missing?(method_name, include_private = false)
    method_name.to_s.start_with?("get_") || super
  end
end

Once you've implemented respond_to_missing?, respond_to? will return true, and your object behaves like a first-class citizen in the Ruby ecosystem. It's a small bit of extra code that separates the amateurs from the pros.




📋 Practical Task

Building a Dynamic Configuration Proxy

Imagine you are building a configuration system where settings are stored in a Hash, but you want to access them using dot-notation (e.g., config.api_key instead of config[:api_key]) for better readability.

Your Task: Create a class called ConfigProxy that takes a hash in its initializer. Implement the following:

  • Use method_missing to allow reading any key from the internal hash as if it were a method.
  • Use define_method inside method_missing so that subsequent calls to the same configuration key are faster.
  • Implement respond_to_missing? so that respond_to? correctly returns true for any key present in the hash.
  • Ensure that calls to methods that are NOT in the hash still raise a NoMethodError.

Test your implementation with this scenario:

settings = { api_key: "secret123", timeout: 30, environment: "production" }
config = ConfigProxy.new(settings)

puts config.api_key     # Should print "secret123"
puts config.respond_to?(:timeout) # Should print true
puts config.environment # Should print "production"
# config.unknown_setting # Should raise NoMethodError
Rating
0 0

There are no comments for now.

to be the first to leave a comment.