Overview
The method_added
hook provides a callback mechanism that Ruby invokes automatically whenever a new instance method gets defined within a class. Ruby calls this hook immediately after method definition completes, passing the method name as a symbol parameter.
Ruby implements method_added
as a private class method that receives one argument - the symbol representing the newly defined method's name. Classes and modules can override this method to execute custom logic whenever new methods appear.
class Example
def self.method_added(method_name)
puts "Method #{method_name} was just defined"
end
def hello
"world"
end
end
# Output: Method hello was just defined
The hook fires for all method definitions including those created through define_method
, alias_method
, and other metaprogramming techniques. Ruby calls the hook in the context where the method gets defined, not where the hook gets defined.
class Parent
def self.method_added(method_name)
puts "Parent caught: #{method_name}"
end
end
class Child < Parent
def instance_method
# Hook executes in Child's context
end
end
# Output: Parent caught: instance_method
Ruby frameworks leverage this hook extensively for implementing domain-specific languages, automatic method decoration, and dynamic behavior injection. Rails Active Record uses similar hooks for attribute methods, while RSpec uses them for test method discovery.
The hook operates at the class level only - Ruby provides no equivalent for singleton methods or global functions. Module inclusion and extension trigger their own separate hooks (included
, extended
) that work alongside method_added
.
Basic Usage
Override method_added
as a class method to receive notifications about new instance method definitions. Ruby calls this method with the new method's name as a symbol immediately after definition completes.
class ValidationClass
@@validations = {}
def self.method_added(method_name)
return unless method_name.to_s.start_with?('validate_')
@@validations[method_name] = true
puts "Registered validator: #{method_name}"
end
def validate_email(email)
email.include?('@')
end
def validate_password(password)
password.length >= 8
end
def regular_method
"not a validator"
end
end
# Output:
# Registered validator: validate_email
# Registered validator: validate_password
The hook receives method names from all definition sources including define_method
and alias_method
. Ruby treats these programmatic definitions identically to standard def
statements.
class DynamicMethods
def self.method_added(method_name)
puts "Added: #{method_name} (#{caller_locations(1, 1).first.label})"
end
define_method(:dynamic_one) { "created dynamically" }
def standard_method
"created normally"
end
alias_method :aliased_method, :standard_method
end
# Output:
# Added: dynamic_one (define_method)
# Added: standard_method (def)
# Added: aliased_method (alias_method)
Inheritance passes hook behavior to subclasses automatically. Child classes inherit parent method_added
implementations unless they override the hook themselves.
class BaseClass
def self.method_added(method_name)
puts "Base class detected: #{method_name}"
end
end
class SubClass < BaseClass
def child_method
"defined in child"
end
end
# Output: Base class detected: child_method
class OverridingSubClass < BaseClass
def self.method_added(method_name)
puts "Child class detected: #{method_name}"
super # Call parent implementation
end
def another_method
"also defined in child"
end
end
# Output:
# Child class detected: another_method
# Base class detected: another_method
Method redefinition triggers the hook again. Ruby does not distinguish between initial definition and redefinition - both fire the callback with identical parameters.
class RedefinitionExample
def self.method_added(method_name)
@definition_count ||= {}
@definition_count[method_name] ||= 0
@definition_count[method_name] += 1
puts "#{method_name} defined #{@definition_count[method_name]} times"
end
def changeable_method
"version 1"
end
def changeable_method
"version 2"
end
end
# Output:
# changeable_method defined 1 times
# changeable_method defined 2 times
Advanced Usage
Complex hook implementations often maintain registries or apply decorations based on method names, parameters, or source locations. Ruby provides access to method objects and caller information within hook implementations.
class MethodRegistry
@method_metadata = {}
@method_patterns = {}
def self.method_added(method_name)
method_obj = instance_method(method_name)
source_location = method_obj.source_location
parameter_names = method_obj.parameters.map(&:last)
@method_metadata[method_name] = {
parameters: parameter_names,
source_file: source_location&.first,
source_line: source_location&.last,
arity: method_obj.arity
}
categorize_method(method_name, method_obj)
end
def self.categorize_method(name, method_obj)
case name.to_s
when /^find_/
@method_patterns[:finders] ||= []
@method_patterns[:finders] << name
when /^validate_/
@method_patterns[:validators] ||= []
@method_patterns[:validators] << name
when /^calculate_/
@method_patterns[:calculators] ||= []
@method_patterns[:calculators] << name
end
end
def self.method_metadata
@method_metadata
end
def self.method_patterns
@method_patterns
end
def find_user_by_email(email)
# Finder method
end
def validate_email_format(email)
# Validator method
end
def calculate_tax_amount(amount, rate)
# Calculator method
end
def regular_helper(data)
# Uncategorized method
end
end
puts MethodRegistry.method_metadata[:find_user_by_email]
# => {:parameters=>[:email], :source_file=>"...", :source_line=>..., :arity=>1}
puts MethodRegistry.method_patterns[:finders]
# => [:find_user_by_email]
Hook chaining through modules creates sophisticated decoration systems. Multiple modules can each contribute hook behavior that combines to create complex method processing pipelines.
module TimingDecorator
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def method_added(method_name)
super if defined?(super)
return if @decorating_method
@decorating_method = true
original_method = instance_method(method_name)
define_method(method_name) do |*args, **kwargs, &block|
start_time = Time.now
result = original_method.bind(self).call(*args, **kwargs, &block)
end_time = Time.now
puts "#{method_name} executed in #{end_time - start_time} seconds"
result
ensure
@decorating_method = false
end
@decorating_method = false
end
end
end
module CachingDecorator
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def method_added(method_name)
super if defined?(super)
return if @adding_cache_method
return unless method_name.to_s.start_with?('expensive_')
@adding_cache_method = true
original_method = instance_method(method_name)
define_method(method_name) do |*args, **kwargs, &block|
cache_key = "#{method_name}_#{args.hash}_#{kwargs.hash}"
@method_cache ||= {}
if @method_cache.key?(cache_key)
puts "Cache hit for #{method_name}"
return @method_cache[cache_key]
end
result = original_method.bind(self).call(*args, **kwargs, &block)
@method_cache[cache_key] = result
puts "Cached result for #{method_name}"
result
end
@adding_cache_method = false
end
end
end
class ProcessingClass
include TimingDecorator
include CachingDecorator
def expensive_calculation(n)
sleep(0.1) # Simulate expensive operation
n * n
end
def regular_method
"not cached or timed"
end
end
processor = ProcessingClass.new
processor.expensive_calculation(5)
# Output:
# Cached result for expensive_calculation
# expensive_calculation executed in 0.1+ seconds
processor.expensive_calculation(5)
# Output: Cache hit for expensive_calculation
Dynamic method generation based on configuration data demonstrates advanced hook usage. Ruby applications can build entire APIs programmatically while maintaining hook-based monitoring and decoration.
class ConfigurableMethods
CONFIG = {
endpoints: [
{ name: 'get_user', http_method: 'GET', path: '/users/:id' },
{ name: 'create_user', http_method: 'POST', path: '/users' },
{ name: 'update_user', http_method: 'PUT', path: '/users/:id' }
]
}
def self.method_added(method_name)
endpoint_config = CONFIG[:endpoints].find { |e| e[:name] == method_name.to_s }
return unless endpoint_config
puts "Configuring endpoint: #{endpoint_config[:http_method]} #{endpoint_config[:path]}"
# Store endpoint metadata for routing
@endpoint_registry ||= {}
@endpoint_registry[method_name] = endpoint_config
# Add parameter validation based on path
add_parameter_validation(method_name, endpoint_config[:path])
end
def self.add_parameter_validation(method_name, path)
required_params = path.scan(/:(\w+)/).flatten.map(&:to_sym)
return if required_params.empty?
original_method = instance_method(method_name)
define_method(method_name) do |*args, **kwargs|
required_params.each do |param|
unless kwargs.key?(param)
raise ArgumentError, "Missing required parameter: #{param}"
end
end
original_method.bind(self).call(*args, **kwargs)
end
end
# Generate methods based on configuration
CONFIG[:endpoints].each do |endpoint|
define_method(endpoint[:name]) do |**params|
puts "Executing #{endpoint[:http_method]} #{endpoint[:path]} with #{params}"
end
end
end
api = ConfigurableMethods.new
api.get_user(id: 123) # Works
# Output:
# Configuring endpoint: GET /users/:id
# Executing GET /users/:id with {:id=>123}
begin
api.get_user # Raises error
rescue ArgumentError => e
puts "Error: #{e.message}"
end
# Output: Error: Missing required parameter: id
Common Pitfalls
Infinite recursion occurs when method_added
implementations define new methods without proper guards. Ruby calls the hook for every method definition, including those created within the hook itself.
# WRONG - Creates infinite recursion
class BadExample
def self.method_added(method_name)
define_method("decorated_#{method_name}") do
"decorated version"
end
# This define_method triggers method_added again!
end
end
# CORRECT - Use guards to prevent recursion
class GoodExample
def self.method_added(method_name)
return if @defining_decorated_method
return if method_name.to_s.start_with?('decorated_')
@defining_decorated_method = true
define_method("decorated_#{method_name}") do
"decorated version"
end
@defining_decorated_method = false
end
end
Method object timing creates subtle bugs when hooks capture methods during definition. The instance_method
call within method_added
returns the method object after definition completes, but accessing method properties may return unexpected values.
class TimingIssue
def self.method_added(method_name)
# Method object exists but may have incomplete information
method_obj = instance_method(method_name)
# Source location is reliable
puts "Source: #{method_obj.source_location}"
# But method body inspection might fail
# method_obj.source # May not work reliably
# Parameters are available
puts "Parameters: #{method_obj.parameters}"
end
def example_method(param1, param2 = nil, *args, **kwargs)
"method body"
end
end
Hook inheritance and super calling requires careful attention to method resolution order. Missing super
calls break hook chains, while incorrect super
usage causes errors.
class Parent
def self.method_added(method_name)
puts "Parent processing: #{method_name}"
end
end
# WRONG - Breaks parent hook chain
class BadChild < Parent
def self.method_added(method_name)
puts "Child processing: #{method_name}"
# Missing super call!
end
end
# CORRECT - Maintains hook chain
class GoodChild < Parent
def self.method_added(method_name)
puts "Child processing: #{method_name}"
super # Calls parent implementation
end
end
# WRONG - Super without defined? check
class ConditionalParent
# Sometimes has method_added, sometimes doesn't
end
class RiskyChild < ConditionalParent
def self.method_added(method_name)
puts "Child processing: #{method_name}"
super # May raise NoMethodError
end
end
# CORRECT - Safe super calling
class SafeChild < ConditionalParent
def self.method_added(method_name)
puts "Child processing: #{method_name}"
super if defined?(super)
end
end
Class variable and instance variable sharing between hook executions causes state pollution. Multiple method definitions can interfere with each other when hooks use shared storage incorrectly.
# WRONG - Shared state causes problems
class StateProblems
@@processing_method = false # Class variable shared across all methods
def self.method_added(method_name)
return if @@processing_method
@@processing_method = true
# Long processing that might be interrupted
sleep(0.1) # Simulate processing
puts "Processed: #{method_name}"
@@processing_method = false
end
end
# Multiple threads or recursive calls can interfere
# CORRECT - Use method-specific guards
class StateFixed
def self.method_added(method_name)
@processing_methods ||= Set.new
return if @processing_methods.include?(method_name)
@processing_methods.add(method_name)
begin
puts "Processed: #{method_name}"
ensure
@processing_methods.delete(method_name)
end
end
end
Module inclusion order affects hook behavior in non-obvious ways. Later included modules can override earlier hook implementations, and module hook behavior differs from class hook behavior.
module FirstHook
def self.included(base)
def base.method_added(method_name)
puts "First hook: #{method_name}"
super if defined?(super)
end
end
end
module SecondHook
def self.included(base)
def base.method_added(method_name)
puts "Second hook: #{method_name}"
super if defined?(super)
end
end
end
class InclusionOrder
include FirstHook
include SecondHook # This overwrites FirstHook's method_added!
def test_method
"testing"
end
end
# Output: Second hook: test_method
# FirstHook's implementation gets lost
# CORRECT - Use module extension pattern
module BetterFirstHook
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def method_added(method_name)
puts "Better first hook: #{method_name}"
super if defined?(super)
end
end
end
Reference
Core Method Signature
Method | Parameters | Context | Description |
---|---|---|---|
method_added(method_name) |
method_name (Symbol) |
Class/Module | Called when instance method gets defined |
Hook Timing and Behavior
Scenario | Hook Fires | Method Available | Notes |
---|---|---|---|
def method_name |
After definition | Yes | Standard definition |
define_method(:name) |
After block execution | Yes | Dynamic definition |
alias_method :new, :old |
After aliasing | Yes | Method aliasing |
Method redefinition | Every redefinition | Yes | No distinction from first definition |
Private method definition | After definition | Yes | Hook fires regardless of visibility |
Singleton method definition | Never | N/A | Hook only applies to instance methods |
Method Object Access
def self.method_added(method_name)
method_obj = instance_method(method_name)
# Available immediately
method_obj.name # => :method_name
method_obj.arity # => parameter count
method_obj.parameters # => parameter info array
method_obj.source_location # => [file, line] or nil
# Binding and execution
bound_method = method_obj.bind(instance)
result = bound_method.call(*args)
end
Inheritance Chain Behavior
Class Hierarchy | Hook Execution | Super Behavior |
---|---|---|
Parent with hook, Child inherits |
Parent hook runs for Child methods | super not needed in Parent |
Child overrides hook, calls super |
Child runs first, then Parent | Explicit super required |
Child overrides hook, no super |
Only Child hook runs | Parent hook skipped |
Multiple module inclusion | Last included wins | Previous hooks lost unless chained |
Common Guard Patterns
# Prevent infinite recursion
def self.method_added(method_name)
return if @defining_wrapper_method
@defining_wrapper_method = true
# ... define new methods ...
@defining_wrapper_method = false
end
# Method name filtering
def self.method_added(method_name)
return unless method_name.to_s.match?(/^api_/)
# ... process API methods only ...
end
# Per-method guards
def self.method_added(method_name)
@processed_methods ||= Set.new
return if @processed_methods.include?(method_name)
@processed_methods.add(method_name)
# ... process method ...
end
Error Handling Patterns
Error Type | Cause | Prevention |
---|---|---|
SystemStackError |
Infinite recursion in hook | Use guard variables |
NoMethodError on super |
Parent has no method_added |
Use super if defined?(super) |
NameError |
Method not yet available | Access via instance_method(name) |
ArgumentError |
Wrong parameter count | Check method arity before calling |
Module Integration Patterns
# Module with hook capability
module HookProvider
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def method_added(method_name)
# Hook implementation
super if defined?(super)
end
end
end
# Composable hook modules
module ChainableHook
def self.included(base)
base.singleton_class.prepend(HookMethods)
end
module HookMethods
def method_added(method_name)
# Process in this module
super # Chain to next hook
end
end
end
Performance Considerations
Pattern | Performance Impact | Recommendation |
---|---|---|
Hook with complex processing | High overhead on every method definition | Cache results, use lazy evaluation |
Method object creation | Moderate overhead | Store method objects only when needed |
Recursive method decoration | Exponential complexity | Use single-pass decoration with guards |
Large hook inheritance chains | Linear overhead | Minimize hook chain depth |