📞 Вызов методов динамически в Ruby: send, __send__ и public_send

Иногда в 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

⚠️ Опасные паттерны

  1. Пользовательский ввод → send
    # Уязвимость!
    params[:action] = "destroy_all"
    model.send(params[:action])
    
  2. Чрезмерное использование в простых случаях
    # Плохо:
    user.send(:name)
       
    # Хорошо:
    user.name
    
  3. Игнорирование 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 — только при осознанном нарушении инкапсуляции (и с комментарием “почему это необходимо”).

🗓 Дата публикации: 22.04.2025, но это не точно...

Ruby метапрограммирование рефлексия динамический-вызов