Ruby
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Methods and Blocks
-
Section 4: Object-Oriented Ruby
-
Section 5: Metaprogramming
-
Section 6: Working with Data and Files
-
Section 7: Ruby Frameworks Overview
-
Section 8: Ecosystem and Testing
-
Section 9: Practical Projects
-
Section 10: Interview Practice
-
Section 11: Data Structures and Algorithms in Ruby
-
Section 12: More Practice Exercises
-
Section 13: Enumerable and Functional Style
-
Section 14: More OOP Practice
-
Section 15: More Testing
-
Section 16: Enumerable and Comparable Modules In Depth
-
Section 17: Ruby Standard Library: Core Utilities
-
Section 18: Ruby Standard Library: Data and Security
-
Section 19: Ruby Standard Library: CLI and Text
-
Section 20: Ruby Networking
-
Section 21: Ruby on Rails Deep Dive
-
Section 22: Ruby Metaprogramming Deep Dive
-
Section 23: Ruby Design Patterns
-
Section 24: Ruby Concurrency
-
Section 25: Ruby Testing Deep Dive
-
Section 26: Ruby Gems and Packaging
-
Section 27: Ruby Performance
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Rails API Development
-
Section 31: Rails Authentication and Authorization
-
Section 32: Rails Testing Deep Dive
-
Section 33: Rails Performance
-
Section 34: Rails Deployment
-
Section 35: Sinatra and Lightweight Ruby Web Apps
-
Section 36: More Ruby Language Deep Dive
-
Section 37: Ruby 3.x Modern Features
-
Section 38: More Data Structures in Ruby
-
Section 39: More Practical Projects
-
Section 40: Ruby Ecosystem Tools
-
Section 41: More Practice and Review
-
Section 42: Final Practice and Mastery
-
Section 43: Ruby Interview Deep Dive
-
Section 44: Ruby Background Processing Deep Dive
-
Section 45: Ruby GraphQL
-
Section 46: Ruby Object Model Deep Dive
-
Section 47: Ruby Hanami Framework Overview
-
Section 48: Ruby gRPC and Protocol Buffers
-
Section 49: Ruby Data Processing
-
Section 50: Ruby Search Integration
-
Section 51: Ruby File Upload and Media
-
Section 52: Ruby Email and Notifications
-
Section 53: Ruby Admin Panels
-
Section 54: Ruby Feature Flags and Experimentation
-
Section 55: Ruby Monitoring and Observability
-
Section 56: Ruby Docker and Deployment Deep Dive
-
Section 57: Ruby Security Deep Dive
-
Section 58: More Advanced Metaprogramming
-
Section 59: More Final Projects
18: method_missing and Dynamic Methods
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_missingto allow reading any key from the internal hash as if it were a method. - Use
define_methodinsidemethod_missingso that subsequent calls to the same configuration key are faster. - Implement
respond_to_missing?so thatrespond_to?correctly returnstruefor 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
There are no comments for now.