Ruby’новые методы define_method, method_missing и instance_eval
февраля 13, 2011 | Published in Ruby, Основы | 4 Comments
Давно хотел написать отдельные статьи по каждому из методов, но вчера нашел очень интересную статью в которой рассказывается о всех трех методах, да так, что мне аж понравилось. Собственно вот ее перевод:
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
Оригинал статьи на английском:
Лучшая благодарность автору — ваши комментарии!
февраля 20, 2011 at 11:48 (#)
Познавательно, особенно про instance_eval. Всегда было интересно как это работает, но руки не доходили самостоятельно разобраться.
февраля 20, 2011 at 15:41 (#)
Еще есть идея написать статью о различиях module_eval, eval, class_eval и instance_eval и несколькими интересными хакерскими примерами кода =)
февраля 20, 2011 at 16:34 (#)
[...] а также обслуживаемые с помощью method_missing [...]
октября 18, 2012 at 12:43 (#)
Этот код выдаст ошибку, у объекта класса Callbacker нет метода define_method. self указывает на объект, а метод находится в модуле Module, который подмешивается к классу Class. Надо заменить на