Ruby’новые методы define_method, method_missing и instance_eval

февраля 13, 2011  |  Published in Ruby, Основы  |  4 Comments

rubyДавно хотел написать отдельные статьи по каждому из методов, но вчера нашел очень интересную статью в которой рассказывается о всех трех методах, да так, что мне аж понравилось. Собственно вот ее перевод:

Ruby’новые методы define_method, method_missing и instance_eval

Одной из вещей, которые мне так нравятся в Ruby есть то, что Ruby очень похож на Lisp. Конечно, Ruby не имеет полноты макросов, но он облает набором абсолютно крутецких методов, которые позволяют сокращать ваш код и делать ваши программы более легкоподдерживаемыми. Три метода, которые я реально обожаю это: define_method, method_missing и instance_eval. В этой статье я хочу подробно рассказать о них.

define_method

Ruby’новый метод define_method позволяет вам создавать методы бысрее, чем использую коючевое слово def. Одно из главных достоинств этого состоит в том , что вы можете сокращать дублирование внутренних методов с похожей функциональной начинкой. Например, все следующие методы получают данные из хэша:

# Без использования метода define_method:
def user;  @data[:user];  end
def email; @data[:email]; end
def food;  @data[:food];  end

С помощью define_method мы можем проходить по каждому имени метода и сокращать дублирование повторяющегося кода:

# Используя метод define_method:
%w(user email food).each do |meth|
  define_method(meth) { @data[meth.to_sym] }
end

Хотя в приведенном выше примере строк кода столько, сколько и в примере с объявлением через def, код выглядит значительно легче для поддержки, чем код в котором мы объявляем каждый метод отдельно. Например, добавление нового метода это такая же простая задача, как добавление нового элемента в массив. Кроме того, если мы решим изменить имя переменной экземпляра класса на @kool_data, то нам следует обновить методы-аксессоры всего в одном месте.

Другое приятное преимущество в том, что методы созданные с помощью define_methods на самом деле являются замыканиями в отличие от обычных Ruby’новых методов. Это дает нам возможность, например, использовать define_method для разрешения создания во время исполнения программы  колбек-методов, как в примере ниже:

class Callbacker
  def make_callback(obj, meth)
    self.send(:define_method, :callback) { obj.send(meth) }
  end
end

# usage
callbacker = Callbacker.new
callbacker.make_callback("   hello   ", :strip)
callbacker.callback   # => "hello"

К сожалению пример весьма надуман, но это то, что первым вырвалось наружу из моей головы=) Если вы придумаете более реальный пример, то прошу вас написать его в комментарии и я добавлю ваш пример в статью.

method_missing

Еще одной отличной возможностью языка Ruby является method_missing. Этот метод позволил создать в Ruby on Rails такую магию, как генерацию find_by_* методов! Вы наверняка уже сталкивались с такими методами, если не помните, то я приведу вам пример:

Post.find_by_title("Awesomeness!")
User.find_by_email("bob@example.com")
User.find_by_email_and_login("bob@example.com", "bob")

Объявление всех этих find_by_* методов в ручную практически невозможно, так как существует большое количество комбинаций основанных на столбцах таблицы базы данных. В таких проблемных ситуациях method_missing реально блистает своей практичностью. Только взгляните:

сlass ActiveRecord::Base
  def method_missing(meth, *args, &block)
    if meth.to_s =~ /^find_by_(.+)$/
      run_find_by_method($1, *args, &block)
    else
      super # Вы должны вызвать super если вы не собираетесь
      # использовать метод, иначе вы засорите поиск методов.
    end
  end

  def run_find_by_method(attrs, *args, &block)
  # Создает массив имен атрибутов
    attrs = attrs.split('_and_')

    # #transpose объединяет два массива вместе, пример:
    #   [[:a, :b, :c], [1, 2, 3]].transpose
    #   # => [[:a, 1], [:b, 2], [:c, 3]]
    attrs_with_args = [attrs, args].transpose

    # Hash[] получает массив и преобразует его в хэш:
    #   Hash[[[:a, 2], [:b, 4]]] # => { :a => 2, :b => 4 }
    conditions = Hash[attrs_with_args]

    # #where и #all это новые вкусности из AREL, которые позволяют найти
    # все записи соответствующие условию поиска
    where(conditions).all
  end
end

Хотя в примере выше много кода, наиболее важная часть заключается в использовании method_missing. здесь мы используем регулярные выражения для проверки того начинается ли строка с «find_by_» и дале строка с именем метода передается в метод run_find_by_method.

Существует три очень важных замечания к использованию method_missing. Во-первых вы должны вызвать super если вы не планируете обращаться к переданному методу. Если вы не вызовете super, товаш код будет вести себя странно.

Во-вторых выполнение метода через method_missing медленнее, чем выполнение нормально определенных методов.

В конце концов вы должны также объявлять соответствующий respond_to? который подтвержает что ваш объект отвечает этим магическим методам. Для примера выше respond_to? будет выглядеть следующим образом:

class ActiveRecord::Base
  def respond_to?(meth)
    if meth.to_s =~ /^find_by_.*$/
      true
    else
      super
    end
  end
end

Хотя respond_to? за частую не используестя на практике, будет хорошей идеей убедиться, что ваш respond_to? воответствует вашему method_missing.

instance_eval

instance_eval это как швейцарский армейский нож, но он реально крут, когда его используют при разработке DSL. Для примера посмотрите подход в Chef API к настройке файловых шаблонов:

template "/path/to/file.conf" do
  source "file.conf.erb"
  owner  "trotter"
  mode   "0755"
end

Внутри метода template мы имеем доступ к методам source, owner и mode, которые не доступны вне метода template. Для того, чтобы реализовать это, вы должны выполнить блок переданный методу template в контексте где методы source, owner и mode определены. С помощью instance_eval это возможно и мы могли бы реализовать подобие Chef API следующим образом:

class ChefDSL
  def template(path, &block)
    TemplateDSL.new(path, &block)
  end
end

class TemplateDSL
  def initialize(path, &block)
    @path = path
    instance_eval &block
  end

  def source(source); @source = source; end
  def owner(owner);   @owner  = owner;  end
  def mode(mode);     @mode   = mode;   end
end

Реальный код в этом примере это instanse_eval в TemplateDSL. Он получает блок кода и запускает его внутри объекта TemplateDSL. Это означает, что блок имеет доступ к методам source, owner и mode из TemplateDSL, который используется для установки значений соответствующих переменных экземпляра класса.

Если бы мы не использовали instance_eval и имели вместо этого объявленный метод initialize как показано внизу, то Ruby вызвал бы ошибку NoMethodError, потому,что методы source, owner и mode не были бы доступны блоку.

class TemplateDSL
  def initialize(path, &block)
    @path = path
    block.call
  end
end

Оригинал статьи на английском:

Лучшая благодарность автору — ваши комментарии!

Tags: , ,

Responses

  1. c0va23 says:

    февраля 20, 2011 at 11:48 (#)

    Познавательно, особенно про instance_eval. Всегда было интересно как это работает, но руки не доходили самостоятельно разобраться.

  2. admin says:

    февраля 20, 2011 at 15:41 (#)

    Еще есть идея написать статью о различиях module_eval, eval, class_eval и instance_eval и несколькими интересными хакерскими примерами кода =)

  3. Rails Tutorial: Модели: Простая выборка данных | Ruby on Rails c нуля! says:

    февраля 20, 2011 at 16:34 (#)

    [...] а также обслуживаемые с помощью method_missing [...]

  4. Duke says:

    октября 18, 2012 at 12:43 (#)

    class Callbacker
      def make_callback(obj, meth)
        self.send(:define_method, :callback) { obj.send(meth) }
      end
    end
    

    Этот код выдаст ошибку, у объекта класса Callbacker нет метода define_method. self указывает на объект, а метод находится в модуле Module, который подмешивается к классу Class. Надо заменить на

    self.class.send(:define_method, :callback){obj.send(meth)}
    

Leave a Response

Для подсветки кода используйте BB - коды: [language]...[/language], где language может быть: ruby, javascript, css, html.