CrackedRuby logo

CrackedRuby

define_method

Overview

Ruby's define_method creates methods dynamically during program execution. The method belongs to the Module class and generates instance methods on the receiving module or class. Unlike def statements, define_method accepts method names as variables and captures local variables from the surrounding scope through closures.

The method accepts either a block or a callable object (Method or Proc) to serve as the method body. When called with a block, define_method creates a closure that retains access to local variables present at definition time. This closure behavior distinguishes define_method from standard method definitions and creates both opportunities and complications in method generation.

Ruby evaluates define_method calls immediately when encountered, making the newly defined method available for use. The method returns a symbol representing the method name, providing confirmation of successful method creation.

class Calculator
  define_method(:add) do |a, b|
    a + b
  end
end

calc = Calculator.new
calc.add(5, 3)
# => 8

Classes and modules inherit define_method from Module, making it available in all Ruby objects that can contain methods. The method operates within the context of its receiver, creating methods that belong to the specific class or module where define_method executes.

module MathOperations
  define_method(:multiply) { |x, y| x * y }
end

class Calculator
  include MathOperations
end

Calculator.new.multiply(4, 6)
# => 24

Basic Usage

The most common define_method pattern accepts a symbol or string for the method name and a block containing the method body. The block parameters become the method parameters, and the block's return value becomes the method's return value.

class User
  attr_accessor :name, :email
  
  define_method(:greeting) do
    "Hello, I'm #{name}"
  end
  
  define_method(:contact_info) do |format = :standard|
    case format
    when :standard
      "#{name} <#{email}>"
    when :formal
      "Name: #{name}, Email: #{email}"
    end
  end
end

user = User.new
user.name = "Alice"
user.email = "alice@example.com"
user.greeting
# => "Hello, I'm Alice"
user.contact_info(:formal)
# => "Name: Alice, Email: alice@example.com"

Method names can come from variables, making define_method valuable for creating methods programmatically. This approach works particularly well when generating similar methods with slight variations.

class DataProcessor
  OPERATIONS = [:upcase, :downcase, :reverse, :strip]
  
  OPERATIONS.each do |operation|
    define_method("process_#{operation}") do |text|
      text.send(operation)
    end
  end
end

processor = DataProcessor.new
processor.process_upcase("hello world")
# => "HELLO WORLD"
processor.process_reverse("hello world")
# => "dlrow olleh"

The method captures local variables from its definition context through closure behavior. These variables remain accessible within the method body even after their original scope disappears.

class ConfigurableGreeter
  def self.create_greeter(prefix, suffix)
    define_method(:greet) do |name|
      "#{prefix} #{name} #{suffix}"
    end
  end
end

ConfigurableGreeter.create_greeter("Welcome", "to our service!")
greeter = ConfigurableGreeter.new
greeter.greet("Bob")
# => "Welcome Bob to our service!"

The method also accepts Method objects and Proc objects instead of blocks. This approach provides flexibility when the method body comes from existing callable objects.

class Calculator
  addition_proc = proc { |a, b| a + b }
  define_method(:add, addition_proc)
  
  subtraction_method = lambda { |a, b| a - b }
  define_method(:subtract, subtraction_method)
end

calc = Calculator.new
calc.add(10, 5)
# => 15
calc.subtract(10, 5)
# => 5

Advanced Usage

Dynamic method creation with define_method supports complex metaprogramming patterns. Method generation can incorporate introspection, configuration data, and runtime conditions to create sophisticated APIs.

Class-level method generation creates methods based on class attributes, configuration, or external data sources. This pattern works well for building domain-specific languages and configuration-driven behavior.

class APIClient
  ENDPOINTS = {
    users: { method: :get, path: '/api/users' },
    user: { method: :get, path: '/api/users/%d' },
    create_user: { method: :post, path: '/api/users' },
    update_user: { method: :put, path: '/api/users/%d' },
    delete_user: { method: :delete, path: '/api/users/%d' }
  }
  
  ENDPOINTS.each do |name, config|
    define_method(name) do |*args|
      path = config[:path] % args
      send_request(config[:method], path, args.last.is_a?(Hash) ? args.last : {})
    end
  end
  
  private
  
  def send_request(method, path, params)
    # HTTP request implementation
    { method: method, path: path, params: params }
  end
end

client = APIClient.new
client.user(123)
# => {:method=>:get, :path=>"/api/users/123", :params=>{}}
client.create_user(name: "Charlie", email: "charlie@example.com")
# => {:method=>:post, :path=>"/api/users", :params=>{:name=>"Charlie", :email=>"charlie@example.com"}}

Method aliasing and delegation patterns use define_method to create wrapper methods with additional behavior. This approach maintains the original method's signature while adding logging, caching, or transformation logic.

class CachedCalculator
  def initialize
    @cache = {}
  end
  
  [:add, :subtract, :multiply, :divide].each do |operation|
    original_method = "#{operation}_impl".to_sym
    
    define_method(original_method) do |a, b|
      case operation
      when :add then a + b
      when :subtract then a - b
      when :multiply then a * b
      when :divide then a.fdiv(b)
      end
    end
    
    define_method(operation) do |a, b|
      key = [operation, a, b]
      @cache[key] ||= send(original_method, a, b)
    end
  end
end

calc = CachedCalculator.new
calc.multiply(6, 7)  # Calculates and caches
# => 42
calc.multiply(6, 7)  # Returns cached value
# => 42

Conditional method definition creates methods based on runtime conditions, feature flags, or environment settings. This pattern enables different implementations for different contexts.

class FeatureToggleService
  FEATURES = {
    advanced_analytics: ENV['ENABLE_ANALYTICS'] == 'true',
    experimental_ui: ENV['ENABLE_EXPERIMENTAL'] == 'true'
  }
  
  FEATURES.each do |feature, enabled|
    if enabled
      define_method("#{feature}_data") do
        # Full feature implementation
        "#{feature.to_s.humanize} data: [complex calculation]"
      end
    else
      define_method("#{feature}_data") do
        # Stub implementation
        "#{feature.to_s.humanize} feature not available"
      end
    end
  end
end

service = FeatureToggleService.new
service.advanced_analytics_data
# => Depends on ENV['ENABLE_ANALYTICS'] setting

Method chaining support through define_method creates fluent interfaces that return self for continued method calls. This pattern works particularly well for configuration and builder objects.

class QueryBuilder
  def initialize
    @conditions = []
    @ordering = []
  end
  
  [:where, :and, :or].each do |condition_type|
    define_method(condition_type) do |field, operator, value|
      @conditions << { type: condition_type, field: field, operator: operator, value: value }
      self
    end
  end
  
  [:asc, :desc].each do |direction|
    define_method("order_#{direction}") do |field|
      @ordering << { field: field, direction: direction }
      self
    end
  end
  
  def build
    {
      conditions: @conditions,
      ordering: @ordering
    }
  end
end

query = QueryBuilder.new
  .where(:name, :eq, "Alice")
  .and(:age, :gt, 18)
  .order_asc(:created_at)
  .build
# => {:conditions=>[...], :ordering=>[...]}

Common Pitfalls

Variable scoping with define_method creates confusion because the method body captures local variables through closures. These variables remain bound to their original values, not their names, leading to unexpected behavior when variables change after method definition.

class ProblematicCounter
  def self.create_counters
    counters = []
    
    # This creates the same problem in all iterations
    5.times do |i|
      define_method("counter_#{i}") do
        i  # This captures the variable i, not its value
      end
      counters << i
    end
    
    counters
  end
end

ProblematicCounter.create_counters
counter = ProblematicCounter.new
counter.counter_0  # Returns 4, not 0!
counter.counter_3  # Returns 4, not 3!
# => All methods return 4 because i's final value was 4

The solution involves creating new variable bindings for each iteration, typically through method parameters or explicit variable assignment.

class CorrectCounter
  def self.create_counters
    5.times do |i|
      # Create new binding by passing i as parameter to a lambda
      counter_lambda = lambda do |index|
        lambda { index }
      end.call(i)
      
      define_method("counter_#{i}", &counter_lambda)
    end
  end
end

CorrectCounter.create_counters
counter = CorrectCounter.new
counter.counter_0  # Returns 0
counter.counter_3  # Returns 3

Instance variable access differs between define_method and regular method definitions. Methods created with define_method execute within the closure's binding first, then fall back to the instance's binding for instance variables.

class VariableAccessDemo
  def initialize
    @instance_var = "instance value"
  end
  
  def regular_method
    local_var = "local in regular method"
    @instance_var
  end
  
  def create_dynamic_method
    local_var = "local in create method"
    
    define_method(:dynamic_method) do
      # This accesses local_var from create_dynamic_method's scope
      "Local: #{local_var}, Instance: #{@instance_var}"
    end
  end
end

demo = VariableAccessDemo.new
demo.create_dynamic_method
demo.dynamic_method
# => "Local: local in create method, Instance: instance value"

Method visibility settings require special attention with define_method. The method inherits the current visibility level at definition time, not at call time.

class VisibilityIssues
  define_method(:public_method) { "public" }
  
  private
  
  define_method(:private_method) { "private" }  # This is private
  
  public
  
  define_method(:another_public) { "public again" }
  
  # This does NOT make private_method public
  # because it was already defined as private
end

obj = VisibilityIssues.new
obj.public_method        # Works
obj.another_public       # Works
obj.private_method       # NoMethodError: private method called

Memory leaks occur when closures capture large objects unnecessarily. The closure keeps references to all local variables in scope, preventing garbage collection of unused objects.

class MemoryLeakExample
  def self.create_processor(large_data_set)
    # large_data_set is captured even if not used
    small_config = large_data_set.config
    
    define_method(:process) do |input|
      # Only small_config is needed, but entire large_data_set remains in memory
      input.transform(small_config.rules)
    end
  end
end

# Better approach: extract only needed data
class EfficientProcessor
  def self.create_processor(large_data_set)
    config = large_data_set.config.dup  # Extract only needed data
    large_data_set = nil  # Explicitly release reference
    
    define_method(:process) do |input|
      input.transform(config.rules)
    end
  end
end

Reference

Core Methods

Method Parameters Returns Description
define_method(name, method = nil, &block) name (Symbol/String), method (Method/Proc), block (Block) Symbol Defines instance method with given name and implementation

Parameter Details

name: Method name as Symbol or String. Symbols perform slightly better due to reduced string allocation.

method: Optional Method or Proc object to use as method body. Cannot be used with block parameter.

block: Block containing method implementation. Block parameters become method parameters.

Return Values

Returns a Symbol representing the defined method name. This symbol can be used for further method manipulation or verification.

result = define_method(:test) { "testing" }
result  # => :test
result.class  # => Symbol

Visibility Control

Methods inherit current visibility level when defined:

Visibility Behavior
public Method accessible from any context (default)
private Method only accessible within same object
protected Method accessible within class hierarchy
class VisibilityExample
  define_method(:public_method) { "public" }
  
  private
  define_method(:private_method) { "private" }
  
  protected  
  define_method(:protected_method) { "protected" }
end

Closure Behavior

Variable Type Access Pattern Persistence
Local variables Captured by reference Persists throughout method lifetime
Instance variables Accessed from instance Standard instance variable behavior
Class variables Accessed from class Standard class variable behavior
Constants Lexical scope lookup Standard constant resolution

Performance Characteristics

Aspect Impact Recommendation
Method definition Slower than def Acceptable for setup/configuration phases
Method execution Comparable to regular methods No runtime penalty after definition
Memory usage Higher due to closure retention Release unnecessary references explicitly
Garbage collection Retained closure variables prevent cleanup Minimize closure scope

Error Conditions

Error Type Cause Example
ArgumentError Both method and block provided define_method(:name, proc {}) { }
ArgumentError Neither method nor block provided define_method(:name)
TypeError Invalid method parameter type define_method(:name, "string")
NameError Invalid method name format define_method("123invalid")

Method Object Types

Type Creation Behavior
Block define_method(:name) { } Creates closure with variable capture
Proc define_method(:name, proc { }) Uses existing Proc object
Lambda define_method(:name, lambda { }) Strict argument checking
Method define_method(:name, method(:existing)) Delegates to existing method

Integration Patterns

Pattern Use Case Implementation
Delegation Forward calls to other objects Store target in closure
Caching Memoize expensive operations Store cache in closure variables
Configuration Create methods from settings Iterate over configuration hash
Aliasing Create alternative method names Define method calling original