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 |