Правило одно для метапрограммирования: не паникуйте!
Как и многие другие, я боролся с термином метапрограммирование. Для целей этой статьи я расширю свое рабочее определение метапрограммирования, включив в него:
Любой код, который значительно повышает уровень абстракции и / или любой код, который создает код. -Мне
Это определение сильно упрощено. Но чтобы начать изучение метапрограммы, у вас должно быть что-то, на что вы можете указать и сказать: «Это метапрограммирование». Я также намеренно отступаю от позиции «Нет метапрограммирования, есть только программирование». Хотя я частично согласен, это не место для таких философских дебатов. Если вам интересны эти дебаты, то есть отличный подкаст Ruby Rogues, в котором несколько выдающихся «Rubyists» обсуждают определение метапрограммирования более подробно. [1]
Начнем с того, что метапрограммирование не волшебно. Это не решит всех ваших проблем. Его использование не сделает вас рок-звездой. Я также собираюсь обойти предупреждение «Вы действительно не должны бегать с ножницами» и предположить, что вы будете применять звуковую логику в своем приложении метапрограммирования. Оставьте в стороне, это может быть прекрасным дополнением к вашему инструментальному поясу. Проще говоря, это способ заставить ваш код сделать для вас немного больше. При правильном использовании это приведет к менее повторяющемуся, более простому тестированию и более чистому коду. Поначалу это может показаться немного запутанным, но как только вы это поймете, вы сможете решить множество проблем, которые в противном случае были бы невероятно сложными. В первой части руководства я расскажу о нескольких хуках класса / модуля. Эти хуки позволят вам улучшить читаемость вашего кода. Иногда это похоже на магию, но это не так.
Если вы заблудились где-то, просто запомните правило № 1 . Кроме того, посмотрите в конце поста ссылки на дополнительные ресурсы / помощь.
Крючки
унаследованный
Первый хук, который я хотел бы обсудить, #inherited
. Это хук, который позволяет такому коду существовать в Rails:
class Post < ActiveRecord::Base | |
has_many :comments | |
end |
Новый разработчик видит это и либо игнорирует его, либо выводит комментарий над class Posts
котором говорится что-то вроде #there be dragons here
. Не правда. «Магия» заключается в том, чтобы полагаться на #inherited
Ruby, предоставляемый #inherited
. Вы не найдете #inherited
на ruby-doc.org, потому что это закрытый метод (который ruby-doc.org не перечисляет). Вместо вызова этого метода вы просто определяете метод и называете его «унаследованным». Вы можете увидеть более подробную информацию на apidoc.com (это также верно для некоторых других методов, перечисленных ниже). Если вы направитесь туда, вы увидите это краткое описание:
Обратный вызов вызывается всякий раз, когда создается подкласс текущего класса.
Предупреждение: резкое упрощение входящего!
Rails делает довольно приятные вещи в отношении создания метода has_many
. Но в конце ActiveRecord :: Base определяет #inherited
, который включает в себя has_many и другие модули, например, [2] :
module ClassMethods | |
def inherited(child_class) #:nodoc: | |
child_class.initialize_generated_modules | |
super | |
end | |
#etc | |
end |
Когда вы наследуете от ActiveRecord :: Base, вышеупомянутый метод запускается. #initialize_generated_modules
настраивает методы / модули и #initialize_generated_modules
их в вашу модель (что позволяет вам создавать свою ассоциацию). Это потенциально немного сбивает с толку. В конце концов это то, что происходит:
class Mammal | |
def self.inherited(child_class) | |
puts «Hey I’m going to be called when I’m inherited» | |
super | |
end | |
end | |
class Dog < Mammal | |
end | |
# | |
=> «Hey I’m going to be called when I’m inherited» |
Довольно круто, имхо. Важно отметить, что #inherited
вызывается до того, как подкласс полностью определен, поэтому любая условная логика, которая включает подкласс, не будет работать. Кроме того, после выполнения вашего пользовательского кода рекомендуется вызывать super
after.
включены
Обратный вызов вызывается всякий раз, когда получатель включен в другой модуль или класс.
Каждый разработчик Ruby знаком с #include
, которое позволяет вам внедрять методы экземпляра модуля в класс. Точно так же, чтобы включить методы класса модуля, вы должны использовать #extend
. Итак, если бы у вас был модуль с методами экземпляра и класса, вам нужно было бы сделать что-то вроде следующего, чтобы получить весь модуль в вашем классе:
class MyClass | |
include Mymodule # instance methods | |
extend Mymodule # class methods | |
end |
Это подводит нас к следующему хуку, который позволяет нам выполнять произвольный код всякий раз, когда модуль входит в класс. Он определен в модуле , и с его использованием связаны некоторые идиомы. Чтобы выполнить предыдущий пример, вы можете просто определить #included
:
module MyModule | |
def self.included(base) | |
base.extend(ClassMethods) | |
end | |
def hellofrominstance | |
puts «hello from #{self}« | |
end | |
# Conventionally named ClassMethods | |
module ClassMethods | |
def hellofromclass | |
puts «Hello from #{self}« | |
end | |
end | |
end | |
# including MyModule will call the hook (#included) which | |
# extends the class methods | |
class Blah | |
include MyModule | |
end | |
pry(main)> Blah.hellofromclass | |
#=> «Hello from Blah» | |
pry(main)> Blah.new.hellofrominstance | |
#=> «hello from #» |
Это только одно использование #included
. Любой код может быть выполнен во включенном хуке, но это обычная идиома, которую вы увидите. Когда мы включили MyModule
в класс Blah
, мы увидели, как срабатывает ловушка, вызовы которой распространяются на «base». В этом случае аргумент «base» представляет текущее « self
которое в конечном итоге становится Blah
называется (да, мы поговорим о self
в другом посте). Милый Бэтмен, верно?
расширенный
Я не буду вдаваться в подробности из-за его сходства с #included
. Он будет запускать произвольный код при расширении модуля. По моему мнению, выполнение обратной (включенной) идиомы менее приемлемо. Тем не менее, я думаю, что это все еще удивительный инструмент. Полностью надуманный пример может выглядеть так:
# execute this snippet | |
module MyModule | |
def self.extended(base) | |
puts «Howdy!» | |
end | |
end | |
class Blah | |
extend MyModule | |
end | |
=> Howdy! | |
=> Blah |
Если вы выполните приведенный выше фрагмент, вы увидите, что #extended
когда Blah расширяет его. Довольно аккуратно. Двигаюсь дальше.
extend_object
Расширяет указанный объект, добавляя константы и методы этого модуля (которые добавляются как одноэлементные методы).
Когда вы вызываете Object # extension extension_object, вызывается обратный вызов. Это особенно полезно, когда вы хотите расширить объект, но только если у него есть определенные характеристики.
module MyModule | |
def self.extend_object(obj) | |
puts «Hello from #{self}« | |
super # important | |
end | |
def self.hello | |
puts «Hello from #{self}« | |
end | |
end | |
pry(main)> (myextendedstring = «blah»).extend(MyModule) | |
#=> Hello from MyModule | |
#=> «blah» | |
pry(main)> s.hello | |
#=> «Hello from blah» |
Это фактически расширит переменную my_extended_string
, которая является строкой «blah», с MyModule.
const_missing
Это очень похоже на method_missing, за исключением того, что оно вызывается, когда константа отсутствует или не определена. Пример, который приводят в документах, довольно волосатый, поэтому я приведу свой очень надуманный пример того, для чего вы можете использовать это.
class MyCustomError < StandardError; end | |
# super terribad | |
def Object.const_missing(name) | |
raise MyCustomError, «You attempted to use a constant that wasn’t defined: #{name}« | |
end | |
pry(main)> WTF | |
#=> MyCustomError: You attempted to use a constant that wasn’t defined: WTF from :3:in `const_missing’ |
Выводы
Эти хуки часто используются в некоторых из самых умных частей Ruby-кода. Использование их может помочь вам написать лучший код. Кроме того, использование этих методов также поможет вам лучше понять, как Ruby выполняет / загружает / управляет каждой написанной вами программой.
В своем выступлении в 2007 году @pragdave объяснил некоторые причины, по которым расширение Ruby с помощью метапрограммирования является одной из основ языка. Он говорил о том, как метапрограммирование в Ruby поднимает уровень абстракции. По сути, это означает увеличение расстояния между машиной / языком и вашим кодом. Это хорошо по нескольким причинам. С одной стороны, код в конечном итоге выглядит гораздо более читабельным как для кодеров, так и для клиентов. Кроме того, после того, как заложен основной код, новый разработчик должен понимать меньше, чтобы иметь возможность внести свой вклад. Этот повышенный уровень абстракции является удивительной частью Ruby, и он широко представлен в некоторых из его крупнейших проектов (особенно в Rails). Я надеюсь, что вы обнаружите, что изучаете хуки, упомянутые выше, в своем собственном коде. Я уверен, что если вы это сделаете, вы начнете думать о Руби чуть более нежно. Спасибо за прочтение.
Это первое издание «Автостопом по метапрограммированию». В следующем выпуске будут рассмотрены основы метапрограммирования в Ruby. Узнайте больше о метапрограммировании в моем блоге, где я разместил несколько ссылок, которые я нашел полезными при написании этого поста. ^ _ ^