Один из методов библиотеки Sidekiq. Объяснение смайла


send в Ruby вызывает методы объектов по имени. Вот очевидный способ применения:


# До: явно используем присваивание. Неудобно, если полей много или они определяются в рантайме.
user.name = "Иван"
user.age = 29

# После: передаём имя атрибута параметром. Решает проблемы первого способа.
def set(field, value)
  send("#{field}=", value)
end
user.set(:name, "Иван")
user.set(:age, 29)

А ещё вы наверняка видели такие строки:


after_create :send_email

Да-да, коллбэки в рельсах внутри реализованы тоже с помощью send.



Ещё через send при тестировании вызывают приватные методы. О том, нужно ли вообще их тестировать, рассказывает Sandi Metz, автор книги Practical Object-Oriented Design in Ruby (о приватных методах с 10:58).


Краткий перевод-пересказ

Она считает тестирование приватных методов излишним: правильный набор входных данных при тестировании публичных методов обеспечит 100%-е покрытие. Но периодически при активной разработке нестабильного кода Sandi нарушает это правило, чтобы не копаться в трейсах, а ловить ошибку в месте возникновения. Такие тесты она считает временными и с готовностью удаляет, когда код стабилизируется. Она в курсе про подход "Если вам нужно тестировать приватный метод, вынесите его в отдельный класс", но считает, что от такого выделения код не станет более стабильным.


Иногда без send не обойтись:


data_point = OpenStruct.new(:queued? => true)
data_point.queued?  # -> true
data_point.send("queued?=",false) # иначе такой метод не вызвать
data_point.queued?  # -> false

Это пример из документации по OpenStruct. Пример надуманный, но встречаются названия и более странные: взять хоть тот же метод из картинки в начале поста.


В продолжение темы о необычных названиях методов


Часто пытаетесь выйти из irb командой учше? Просто добавьте в ~/.irbrc следующий код:


module Kernel
  def учше
    exit
  end
end

Пришло в голову, пока обдумывал примеры для статьи; добавил ради теста себе в .irbrc — работает. Собирался было удалить, потом думаю: "А ведь удобно же". Пока оставил, пускай полежит недельку.


Для отчаянных: зачем останавливаться только на переводе учше-exit?
module Kernel
  def method_missing(method_name, *arguments, &block)
    return(super) unless contains_russian_letters?(method_name.to_s)
    possible_meaning = translit(method_name.to_s)
    send(possible_meaning, *arguments)
  end

  private

  def translit(string)
    string.chars.map do |char|
      russian_to_english_mapping[char] || char
    end.join
  end

  def contains_russian_letters?(string)
    !(string.chars & russian_symbols).empty?
  end

  def russian_symbols
    "йцукенгшщзхъфывапролджэячсмитьбю".chars
  end

  def russian_to_english_mapping
    english = "qwertyuiop[]asdfghjkl;'zxcvbnm,.".chars
    russian_symbols.zip(english).to_h
  end
end

# Пробуем:
puts(1.inspect) # Выводит 1
згеы(1.штызусе) # Тоже выводит. Работает!

Оставлять это не решился.


Недостатки


Неаккуратное использование send ухудшает читаемость. Сравните:


# Вариант 1. Читаемо, но не масштабируемо
if params[:sort_by] == "age"
  users.sort_by_age
else
  users.sort_by_name
end

# Вариант 2. Масштабируемо, но нечитаемо (в примере параметры не фильтруются)
sorting_method = "sort_by_#{params[:sort_by]}"
users.send(sorting_method)

# Лучше переписать код так, чтобы можно было писать 
# и масштабируемо, и читаемо:
users.sort_by(params[:sort_by])

Подведём итоги


Используйте send, если вам нужно:


  • Определять имя метода во время исполнения;
  • Писать свои велосипедные коллбэки;
  • Вызывать приватные методы в тестах.

Не используйте send без необходимости.

Поделиться с друзьями
-->

Комментарии (3)


  1. am-amotion-city
    27.12.2016 12:44
    +3

    В подавляющем большинстве случаев тут нужно вызывать Kernel#public_send. Использовние send вместо public_send — всегда признак попахивающего кода.


    Пример из недостатков вообще ни в какие ворота: на дворе не 2008 год и даже рельсы больше не вызывают подобные методы динамически. Правильный вариант:


    SORT_ALLOWED = %i|age name|
    
    SORT_ALLOWED.each do |m|
      define_method "sort_by_#{m}" do
        users.public_send "sort_by_#{m}"
      end
    end

    Или даже:


    SORT_ALLOWED = %i|age name|
    
    SORT_ALLOWED.each do |m|
      class_eval <<-SORTER, __FILE__, __LINE__ + 1
        def sort_by_#{m}
          users.sort_by_#{m}
        end
      end
      SORTER
    end


    1. HedgeSky
      27.12.2016 13:07

      Вы правы по поводу использования public_send.
      По поводу сортировки: ваши примеры показывают, как можно определить методы sort_by_age и sort_by_name. В статье же рассматриваются разные способы вызова уже определённых методов.
      Ещё пример из статьи упрощённый: он не фильтрует параметры (добавлю упоминание об этом), но обнажает возможную проблему с читаемостью.


  1. Dreyk
    27.12.2016 18:56
    +1

    Для меня мысль "о, а тут мы просто сделаем массив из имен методов и сделаем им всем send" — верный признак того, что я одной ногой в кроличьей норе метапрограммирования, и пора спросить себя, уверен ли я в том, что делаю (я конечно же всегда отвечаю на этот вопрос "конечно же да!")


    Такие штуки позволяют Руби быть просто офигительным языком, просто надо быть аккуратным =) Providing sharp knives, как говорит DHH