CrackedRuby logo

CrackedRuby

method_added

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