Method Hooks & Callbacks
Why Master Method Hooks & Callbacks?
Method hooks and callbacks are Ruby's mechanism for intercepting and responding to method lifecycle events. They enable powerful metaprogramming patterns used in frameworks like Rails, provide debugging capabilities, and allow dynamic behavior modification.
Framework Building
Rails callbacks, validations, lifecycle hooks
Dynamic Proxies
API wrappers, delegation patterns, method forwarding
Debugging & Monitoring
Method tracing, performance monitoring, debugging tools
method_added Hook
The method_added hook is called whenever a new method is defined in a class or module. This enables automatic registration, validation, or decoration of methods as they're created.
Basic method_added Usage
class ApiEndpoint
def self.method_added(method_name)
puts "New API endpoint defined: #{method_name}"
# Automatically register the endpoint
endpoints << method_name unless method_name == :endpoints
end
def self.endpoints
@endpoints ||= []
end
def get_users
# Implementation for /users endpoint
end
def post_user
# Implementation for POST /user
end
end
# Output:
# New API endpoint defined: get_users
# New API endpoint defined: post_user
puts ApiEndpoint.endpoints
# => [:get_users, :post_user]
Advanced: Method Decoration with Timing
class TimedClass
def self.method_added(method_name)
return if @adding_method || method_name.to_s.start_with?('timed_')
# Store original method
original_method = instance_method(method_name)
# Prevent infinite recursion
@adding_method = true
# Replace with timing wrapper
define_method(method_name) do |*args, &block|
start_time = Time.now
result = original_method.bind(self).call(*args, &block)
end_time = Time.now
puts "#{method_name} executed in #{(end_time - start_time) * 1000}ms"
result
end
@adding_method = false
end
def slow_operation
sleep(0.1)
"Done!"
end
def fast_operation
42 * 2
end
end
obj = TimedClass.new
obj.slow_operation # slow_operation executed in 100.5ms
obj.fast_operation # fast_operation executed in 0.02ms
⚠️ Important Considerations
- Watch for infinite recursion when defining methods inside method_added
- method_added is called for every method, including attr_accessor generated methods
- Use flags or naming conventions to prevent unintended hooks
- Performance impact: method_added runs during class definition, not method execution
method_missing Hook
method_missing is called when Ruby cannot find a method. It's the foundation for dynamic method handling, proxy objects, and flexible APIs. Always pair it with respond_to_missing? for proper behavior.
Dynamic Attribute Access
class DynamicConfig
def initialize(data = {})
@data = data
end
def method_missing(method_name, *args, &block)
method_str = method_name.to_s
if method_str.end_with?('=')
# Setter method
key = method_str.chomp('=').to_sym
@data[key] = args.first
elsif method_str.end_with?('?')
# Predicate method
key = method_str.chomp('?').to_sym
!!@data[key]
elsif @data.key?(method_name)
# Getter method
@data[method_name]
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
method_str = method_name.to_s
method_str.end_with?('=', '?') || @data.key?(method_name) || super
end
end
config = DynamicConfig.new
config.database_url = "postgresql://localhost/myapp"
config.debug_mode = true
puts config.database_url # => "postgresql://localhost/myapp"
puts config.debug_mode? # => true
puts config.respond_to?(:debug_mode?) # => true
API Client with Dynamic Methods
class ApiClient
SUPPORTED_METHODS = %w[get post put delete patch].freeze
def initialize(base_url)
@base_url = base_url
end
def method_missing(method_name, *args, &block)
method_str = method_name.to_s
# Handle HTTP method + resource patterns (e.g., get_users, post_user)
if match = method_str.match(/^(#{SUPPORTED_METHODS.join('|')})_(.+)$/)
http_method = match[1]
resource = match[2]
send_request(http_method, resource, *args)
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
method_str = method_name.to_s
SUPPORTED_METHODS.any? { |method| method_str.start_with?("#{method}_") } || super
end
private
def send_request(method, resource, params = {})
url = "#{@base_url}/#{resource}"
puts "#{method.upcase} #{url} with #{params}"
# Actual HTTP request would go here
{ status: 200, data: "Response for #{method} #{resource}" }
end
end
client = ApiClient.new("https://api.example.com")
client.get_users(limit: 10) # GET https://api.example.com/users
client.post_user(name: "John") # POST https://api.example.com/user
client.put_profile(id: 1, bio: "...") # PUT https://api.example.com/profile
puts client.respond_to?(:get_posts) # => true
puts client.respond_to?(:invalid_method) # => false
respond_to_missing?
respond_to_missing? should always be implemented alongside method_missing. It ensures that respond_to? correctly reports whether an object can handle a method call, maintaining Ruby's introspection capabilities.
Proper Implementation Pattern
class SmartProxy
def initialize(target)
@target = target
end
def method_missing(method_name, *args, &block)
if @target.respond_to?(method_name)
puts "Proxying #{method_name} to target"
@target.send(method_name, *args, &block)
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
@target.respond_to?(method_name, include_private) || super
end
end
class Calculator
def add(a, b)
a + b
end
def multiply(a, b)
a * b
end
end
calc = Calculator.new
proxy = SmartProxy.new(calc)
puts proxy.respond_to?(:add) # => true
puts proxy.respond_to?(:subtract) # => false
proxy.add(5, 3) # Proxying add to target => 8
# proxy.subtract(5, 3) # NoMethodError
✅ Best Practices
- Always implement respond_to_missing? when using method_missing
- Call super for unhandled methods to maintain proper error behavior
- Be specific about which methods you'll handle to avoid unexpected behavior
- Consider performance: method_missing is slower than regular method dispatch
Advanced Hook Patterns
Method Delegation with Validation
class ValidatedDelegate
def initialize(target, allowed_methods = [])
@target = target
@allowed_methods = allowed_methods.map(&:to_sym)
end
def method_missing(method_name, *args, &block)
unless @allowed_methods.include?(method_name)
raise NoMethodError, "Method #{method_name} not allowed on this delegate"
end
unless @target.respond_to?(method_name)
raise NoMethodError, "Target doesn't respond to #{method_name}"
end
@target.send(method_name, *args, &block)
end
def respond_to_missing?(method_name, include_private = false)
@allowed_methods.include?(method_name) &&
@target.respond_to?(method_name, include_private)
end
def allowed_methods
@allowed_methods
end
end
string = "Hello World"
delegate = ValidatedDelegate.new(string, [:upcase, :downcase, :length])
puts delegate.upcase # => "HELLO WORLD"
puts delegate.length # => 11
# delegate.reverse # NoMethodError: Method reverse not allowed
Method Registry with Callbacks
module CallbackRegistry
def self.included(base)
base.extend(ClassMethods)
base.class_eval do
@callbacks = Hash.new { |h, k| h[k] = [] }
end
end
module ClassMethods
def method_added(method_name)
super
trigger_callbacks(:after_method_added, method_name)
end
def on_method_added(&block)
@callbacks[:after_method_added] << block
end
def callbacks
@callbacks
end
private
def trigger_callbacks(event, *args)
@callbacks[event].each { |callback| callback.call(*args) }
end
end
end
class MonitoredClass
include CallbackRegistry
on_method_added do |method_name|
puts "Method #{method_name} was added to #{self.name}"
end
on_method_added do |method_name|
# Could register with monitoring system, update documentation, etc.
puts "Registering #{method_name} with monitoring system"
end
def business_method
"Important business logic"
end
def another_method
"More functionality"
end
end
# Output:
# Method business_method was added to MonitoredClass
# Registering business_method with monitoring system
# Method another_method was added to MonitoredClass
# Registering another_method with monitoring system
Real-world Applications
Rails ActiveRecord
Uses method_missing for dynamic finders like find_by_name, association methods, and attribute accessors.
User.find_by_email("john@example.com")
User.find_by_name_and_age("John", 30)
Configuration DSLs
method_added hooks automatically register configuration methods and validate DSL syntax.
config.database :postgresql
config.cache :redis
config.background_jobs :sidekiq
API Clients
Dynamic method creation for REST endpoints, reducing boilerplate and enabling flexible API interactions.
client.get_users
client.post_order(item: "widget")
client.delete_session
Performance & Security Considerations
Performance Impact
- method_missing is slower: Ruby must traverse the method lookup chain completely before calling it
- Consider define_method: For repeated patterns, define actual methods instead of relying on method_missing
- Cache method checks: Store respond_to? results if checking the same methods repeatedly
- Profile carefully: method_missing can become a bottleneck in performance-critical code
Security Considerations
- Input validation: Always validate method names and arguments in method_missing
- Limit scope: Be explicit about which methods you'll handle dynamically
- Avoid eval: Never use eval or instance_eval with user input in method hooks
- Access control: Respect private/protected method boundaries
Safe method_missing Implementation
class SafeApiProxy
ALLOWED_PATTERNS = /\A(get|post|put|delete)_[a-z_]+\z/
def method_missing(method_name, *args, &block)
method_str = method_name.to_s
# Validate method name pattern
unless method_str.match?(ALLOWED_PATTERNS)
raise NoMethodError, "Unsafe method name: #{method_name}"
end
# Validate arguments
args.each do |arg|
unless arg.is_a?(Hash) || arg.is_a?(String) || arg.is_a?(Numeric)
raise ArgumentError, "Unsafe argument type: #{arg.class}"
end
end
# Safe to proceed
execute_api_call(method_name, *args, &block)
end
def respond_to_missing?(method_name, include_private = false)
method_name.to_s.match?(ALLOWED_PATTERNS) || super
end
private
def execute_api_call(method, *args)
# Implementation here
end
end
Common Pitfalls & Solutions
❌ Infinite Recursion in method_added
# BAD - causes infinite recursion
def self.method_added(name)
define_method("wrapped_#{name}") { ... }
end
# GOOD - use a flag to prevent recursion
def self.method_added(name)
return if @defining_wrapper
@defining_wrapper = true
define_method("wrapped_#{name}") { ... }
@defining_wrapper = false
end
❌ Forgetting respond_to_missing?
# BAD - breaks introspection
def method_missing(name, *args)
handle_dynamic_method(name, *args)
end
# GOOD - implement both methods
def method_missing(name, *args)
handle_dynamic_method(name, *args)
end
def respond_to_missing?(name, include_private = false)
can_handle_dynamic_method?(name) || super
end