Иногда в Ruby нужно вызвать метод, имя которого неизвестно до момента выполнения программы — например, при обработке пользовательского ввода или реализации плагинной архитектуры. В этой статье разберём три способа динамического вызова методов: send, __send__ и public_send, их различия и подводные камни.
🧠 Теория: зачем это нужно?
Представьте, что вы пишете:
user.name # => "Анна"
Но что, если имя метода (:name) приходит как строка из базы данных, API или конфига? Вот где на помощь приходят динамические вызовы.
🔧 1. send — “швейцарский нож”
Как работает:
Вызывает любой метод объекта, включая приватные.
class User
private
def secret_code
"123-456"
end
end
user = User.new
user.send(:secret_code) # => "123-456" (несмотря на private!)
Когда использовать:
- В тестах (но лучше
allow_any_instance_of) - При глубокой работе с метапрограммированием
- В DSL-библиотеках
Опасность:
Нарушает инкапсуляцию. Может сломать логику класса.
🛡 2. __send__ — “защищённый брат”
Как работает:
То же, что send, но защищён от переопределения.
class Hacker
def send(*)
"Взломано!"
end
end
obj = Hacker.new
obj.send(:object_id) # => "Взломано!"
obj.__send__(:object_id) # => 70353864381040 (реальный ID)
Когда использовать:
- Когда есть риск, что кто-то переопределил
send - В критически важном коде
Фишка:
Используется внутри Ruby (например, в method_missing).
🔒 3. public_send — “вежливый гость”
Как работает:
Вызывает только публичные методы.
class BankAccount
private
def balance
@balance
end
end
account = BankAccount.new
account.public_send(:balance) # => NoMethodError
Когда использовать:
- При работе с пользовательским вводом
- В API-клиентах
- Когда важно соблюдать инкапсуляцию
Безопасность:
Не вызовет приватные методы, даже если очень попросить.
🎯 Жизненные примеры
1. Динамический рендеринг в Rails
# Вместо:
case action
when "show" then render_show
when "edit" then render_edit
end
# Можно:
public_send("render_#{action}") if respond_to?("render_#{action}")
2. Плагинная система
plugins = [:twitter, :facebook]
plugins.each { |name| public_send("connect_#{name}") }
3. Тестирование приватных методов (антипаттерн, но иногда нужно)
describe "#calculate_tax" do
it "returns correct value" do
expect(calculator.send(:calculate_tax, 100)).to eq(20)
end
end
⚠️ Опасные паттерны
- Пользовательский ввод → send
# Уязвимость! params[:action] = "destroy_all" model.send(params[:action]) - Чрезмерное использование в простых случаях
# Плохо: user.send(:name) # Хорошо: user.name - Игнорирование respond_to?
Всегда проверяйте:if user.respond_to?(method_name) user.public_send(method_name) end
🎤 Что сказать на собеседовании
— В чём разница между
sendиpublic_send?
— send может вызвать любой метод, включая приватные, что нарушает инкапсуляцию. public_send — только публичные, что безопаснее. А __send__ — это защищённая версия send на случай, если его переопределили.
🧾 Вывод
| Метод | Приватные методы? | Защита от переопределения | Безопасность |
|---|---|---|---|
send |
Да | Нет | Низкая |
__send__ |
Да | Да | Средняя |
public_send |
Нет | Нет | Высокая |
Золотое правило:
Используйте public_send для работы с внешними данными, __send__ — когда важно избежать переопределения, а обычный send — только при осознанном нарушении инкапсуляции (и с комментарием “почему это необходимо”).