How to call methods dynamically based on their name?
There are multiple ways to accomplish dynamic dispatch in Ruby, each with their own advantages and disadvantages. Care should be taken to select the most appropriate method for the situation.
The following table breaks down some of the more common techniques:
+---------------+-----------------+-----------------+------------+------------+
| Method | Arbitrary Code? | Access Private? | Dangerous? | Fastest On |
+---------------+-----------------+-----------------+------------+------------+
| eval | Yes | No | Yes | TBD |
| instance_eval | Yes | No | Yes | TBD |
| send | No | Yes | Yes | TBD |
| public_send | No | No | Yes | TBD |
| method | No | Yes | Yes | TBD |
+---------------+-----------------+-----------------+------------+------------+
Arbitrary Code
Some techniques are limited to calling methods only, while others can execute basically anything. Methods that allow execution of arbitrary code should be used with extreme caution, if not avoided altogether.
Access Private
Some techniques are limited to calling public methods only, while others can call both public and private methods. Ideally, you should strive to use the method with the least amount of visibility that fulfills your requirements.
Note: If a technique can execute arbitrary code, it can easily be used to access private methods that it might not otherwise have access to.
Dangerous
Just because a technique can not execute arbitrary code or call a private method does not mean that it is safe, particularly if you are using user-provided values. Delete is a public method.
Fastest on
Some of these techniques may be more performant than others, depending on your Ruby version. Benchmarks to follow....
Examples
class MyClass
def foo(*args); end
private
def bar(*args); end
end
obj = MyClass.new
eval
eval('obj.foo') #=> nil
eval('obj.bar') #=> NoMethodError: private method `bar' called
# With arguments:
eval('obj.foo(:arg1, :arg2)') #=> nil
eval('obj.bar(:arg1, :arg2)') #=> NoMethodError: private method `bar' called
instance_eval
obj.instance_eval('foo') #=> nil
obj.instance_eval('bar') #=> nil
# With arguments:
obj.instance_eval('foo(:arg1, :arg2)') #=> nil
obj.instance_eval('bar(:arg1, :arg2)') #=> nil
send
obj.send('foo') #=> nil
obj.send('bar') #=> nil
# With arguments:
obj.send('foo', :arg1, :arg2) #=> nil
obj.send('bar', :arg1, :arg2) #=> nil
public_send
obj.public_send('foo') #=> nil
obj.public_send('bar') #=> NoMethodError: private method `bar' called
# With arguments:
obj.public_send('foo', :arg1, :arg2) #=> nil
obj.public_send('bar', :arg1, :arg2) #=> NoMethodError: private method `bar' called
method
obj.method('foo').call #=> nil
obj.method('bar').call #=> nil
# With arguments:
obj.method('foo').call(:arg1, :arg2) #=> nil
obj.method('bar').call(:arg1, :arg2) #=> nil
What you want to do is called dynamic dispatch. It’s very easy in Ruby, just use public_send
:
method_name = 'foobar'
obj.public_send(method_name) if obj.respond_to? method_name
If the method is private/protected, use send
instead, but prefer public_send
.
This is a potential security risk if the value of method_name
comes from the user. To prevent vulnerabilities, you should validate which methods can be actually called. For example:
if obj.respond_to?(method_name) && %w[foo bar].include?(method_name)
obj.send(method_name)
end