Ruby send and define_method — Dynamic Dispatch and Method Creation Explained
In this tutorial, you will learn about Ruby send and define_method. We cover key concepts, practical examples, and best practices to help you master this topic.
Ruby send and define_method provide dynamic dispatch for calling methods by name at runtime and runtime method creation for generating methods programmatically based on data.
What You'll Learn
- Using send for dynamic method calls
- Creating methods with define_method
- Performance considerations
- Security best practices
Why It Matters
Dynamic dispatch is core to Rails and Metaprogramming. Doda Browser uses send for plugin loading. Durga Antivirus Pro uses define_method for scanner rule generation. Understanding these powers Rails' belongs_to, has_many, and validates macros.
Real-World Use
Rails' find_by_email, find_by_name_and_email — all generated by method_missing. ActiveRecord associations define methods dynamically. Form builders generate field methods.
flowchart LR
A["send/define_method"] --> B["send"]
B --> C["public_send"]
C --> D["define_method"]
D --> E["Dynamic Getters/Setters"]
A:::current --> B
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#dbeafe,stroke:#2563eb,color:#1e40af
send Basics
class Calculator
def add(a, b) = a + b
def subtract(a, b) = a - b
def multiply(a, b) = a * b
end
calc = Calculator.new
# Call by symbol
puts calc.send(:add, 5, 3) # 8
puts calc.send(:subtract, 10, 4) # 6
# Call by string
puts calc.send("multiply", 6, 7) # 42
# Dynamic method name from variable
op = :add
puts calc.send(op, 5, 3) # 8
send with Dynamic Arguments
def send_request(endpoint, method: :get, params: {})
# Simulate API call
puts "#{method.upcase} #{endpoint} with #{params}"
end
# Dynamic dispatch with hash
endpoint = "/users"
options = { method: :post, params: { name: "Alice" } }
send_request(endpoint, **options) # POST /users with {:name=>"Alice"}
public_send for Safety
class SecureClass
def public_method
"public"
end
private
def secret_method
"secret"
end
end
obj = SecureClass.new
# send bypasses privacy
puts obj.send(:public_method) # public
puts obj.send(:secret_method) # secret — bypasses private!
# public_send respects privacy
puts obj.public_send(:public_method) # public
# obj.public_send(:secret_method) # NoMethodError
define_method
class DynamicAccessors
ATTRIBUTES = %i[name email phone role]
ATTRIBUTES.each do |attr|
define_method(attr) do
instance_variable_get("@#{attr}")
end
define_method("#{attr}=") do |value|
instance_variable_set("@#{attr}", value)
end
define_method("#{attr}?") do
!!instance_variable_get("@#{attr}")
end
end
end
user = DynamicAccessors.new
user.name = "Alice"
user.email = "alice@test.com"
puts user.name # Alice
puts user.email? # true
puts user.phone? # false
define_method with Blocks
class MathOperations
[:add, :subtract, :multiply, :divide].each do |op|
define_method(op) do |a, b|
case op
when :add then a + b
when :subtract then a - b
when :multiply then a * b
when :divide then a / b
end
end
end
# More concise using send
OPERATORS = {
add: :+, subtract: :-, multiply: :*, divide: :/
}
OPERATORS.each do |name, operator|
define_method(name) do |a, b|
a.send(operator, b)
end
end
end
calc = MathOperations.new
puts calc.add(10, 5) # 15
puts calc.multiply(3, 4) # 12
Dynamic Finders Pattern
class Repository
def initialize
@records = []
end
def add(record)
@records << record
end
def method_missing(name, *args)
if name.to_s =~ /^find_by_(.+)$/
field = $1
@records.select { |r| r[field.to_sym] == args.first }
else
super
end
end
def respond_to_missing?(name, include_private = false)
name.to_s.start_with?("find_by_") || super
end
end
repo = Repository.new
repo.add({ name: "Alice", role: "admin" })
repo.add({ name: "Bob", role: "user" })
repo.add({ name: "Charlie", role: "admin" })
puts repo.find_by_name("Alice").inspect
# [{:name=>"Alice", :role=>"admin"}]
puts repo.find_by_role("admin").size # 2
Delegator Pattern
class Delegator
def initialize(target)
@target = target
end
def method_missing(name, *args, &block)
if @target.respond_to?(name)
puts "[DELEGATE] #{name}"
@target.send(name, *args, &block)
else
super
end
end
def respond_to_missing?(name, include_private = false)
@target.respond_to?(name) || super
end
end
class Printer
def print(msg) = puts "Print: #{msg}"
def scan = "scanning..."
end
delegator = Delegator.new(Printer.new)
delegator.print("Hello") # [DELEGATE] print \n Print: Hello
result = delegator.scan # [DELEGATE] scan
puts result # scanning...
Performance Considerations
require "benchmark"
class Calculator
def add(a, b) = a + b
end
calc = Calculator.new
n = 10_000_000
Benchmark.bm do |x|
x.report("normal:") { n.times { calc.add(1, 2) } }
x.report("send: ") { n.times { calc.send(:add, 1, 2) } }
x.report("public:") { n.times { calc.public_send(:add, 1, 2) } }
end
# Example output:
# user system total real
# normal: 0.421827 0.000295 0.422122 (0.422570)
# send: 0.553596 0.000249 0.553845 (0.554233)
# public: 0.575230 0.000260 0.575490 (0.575825)
send is about 30% slower than direct calls. Define methods statically when in hot paths.
Common Mistakes
1. send with User Input
# Dangerous — user can call any method
def call_method(method_name)
obj.send(method_name) # Could call :destroy, :delete_all
end
# Safe — whitelist
ALLOWED_METHODS = %i[name email age]
def call_safe(method_name)
if ALLOWED_METHODS.include?(method_name.to_sym)
obj.send(method_name)
end
end
2. Forgetting public_send for APIs
# Bad — exposes private methods
def api_call(klass, method, *args)
klass.send(method, *args)
end
# Good — respects privacy
def api_call(klass, method, *args)
klass.public_send(method, *args)
end
3. define_method in a Loop Without Closure
# All methods reference the last value of i!
%w[upcase downcase capitalize].each do |_|
define_method("transform_#{_}_wrong") do |str|
str.send(_)
end
end
# Actually this works because the block captures _ correctly in Ruby 2.0+
4. Overusing send Instead of Polymorphism
# Bad — manual dispatch
def process(shape)
shape.send("calculate_#{shape.type}_area")
end
# Good — polymorphism
def process(shape)
shape.area
end
5. Not Handling Missing Methods
# Bad — silent failure
result = obj.send(method_name) rescue nil
# Good — check first
if obj.respond_to?(method_name)
obj.send(method_name)
end
Practice Questions
1. What does send do in Ruby?
Calls a method by name (string or symbol). obj.send(:method_name, args) is equivalent to obj.method_name(args) but the method name can be dynamic.
2. What's the difference between send and public_send?
send can call any method including private and protected. public_send only calls public methods. Use public_send for safer dynamic dispatch.
3. When should you use define_method?
When creating families of similar methods dynamically (accessors, finders, delegators). Define methods at class load time, not in hot paths.
4. Is send slower than direct method calls?
Yes, about 20-30% slower. For most applications this is negligible. Avoid send in performance-critical loops processing millions of iterations.
Challenge: Create a MethodRegistry that uses define_method to register and call named operations dynamically, supporting addition and removal of operations at runtime.
Solution
class MethodRegistry
def initialize
@operations = {}
end
def register(name, &block)
@operations[name] = block
self.class.define_method(name) do |*args|
@operations[name]&.call(*args)
end
end
def unregister(name)
@operations.delete(name)
end
def method_missing(name, *args)
if @operations.key?(name)
@operations[name].call(*args)
else
super
end
end
def respond_to_missing?(name, include_private = false)
@operations.key?(name) || super
end
end
registry = MethodRegistry.new
registry.register(:double) { |x| x * 2 }
registry.register(:greet) { |name| "Hello, #{name}!" }
puts registry.double(5) # 10
puts registry.greet("Alice") # Hello, Alice!
puts registry.respond_to?(:double) # true
registry.unregister(:double)
puts registry.respond_to?(:double) # false
FAQ
{{< faq question="Is send safe to use?" >}}
Yes, when used with trusted method names. Never pass user input directly to send. Whitelist allowed methods or use public_send for public API calls.
{{< /faq >}}
{{< faq question="Can I use send with blocks?" >}}
Yes. obj.send(:method_name, *args) { |x| x * 2 } passes the block to the target method. The block is passed as a Proc parameter.
{{< /faq >}}
{{< faq question="What's the difference between define_method and def?" >}}
define_method creates methods at runtime with dynamic names. def creates methods at parse time with static names. define_method can capture surrounding scope (closures).
{{< /faq >}}
{{< faq question="Can I undefine a method created with define_method?" >}}
Yes. Use undef_method(:method_name) in the class to remove it, or remove_method(:method_name) to keep inherited versions.
{{< /faq >}}
{{< faq question="How does Rails use send?" >}}
Rails uses send extensively: find_by_* dynamic finders, association proxies, update_attribute, send in callbacks, and metaprogramming throughout Active Record and Action Pack.
{{< /faq >}}
Try It Yourself
# send_demo.rb
class DynamicFormatter
FORMATTERS = {
upcase: ->(s) { s.upcase },
downcase: ->(s) { s.downcase },
reverse: ->(s) { s.reverse },
titleize: ->(s) { s.split.map(&:capitalize).join(" ") }
}
FORMATTERS.each do |name, formatter|
define_method(name) do |text|
formatter.call(text)
end
end
end
formatter = DynamicFormatter.new
puts formatter.upcase("hello world") # HELLO WORLD
puts formatter.titleize("ruby is fun") # Ruby Is Fun
puts formatter.reverse("hello") # olleh
# Using send dynamically
text = "dynamic dispatch"
[:upcase, :downcase, :reverse].each do |fmt|
puts formatter.send(fmt, text)
end
What's Next
Now that you understand dynamic dispatch, explore method_missing for building ghost methods and dynamic proxies.
| Topic | Description | Link |
|---|---|---|
| Ruby method_missing | Ghost methods, dynamic proxies | {{< ref "36-method-missing" >}} |
| Ruby DSL | Building fluent interfaces | {{< ref "34-dsl" >}} |
| Python getattr | Compare Python's dynamic dispatch | Python |
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro