Статьи

Руководство автостопом по метапрограммированию: зацепки класса / модуля

Правило одно для метапрограммирования: не паникуйте!

Как и многие другие, я боролся с термином метапрограммирование. Для целей этой статьи я расширю свое рабочее определение метапрограммирования, включив в него:

Любой код, который значительно повышает уровень абстракции и / или любой код, который создает код. -Мне

Это определение сильно упрощено. Но чтобы начать изучение метапрограммы, у вас должно быть что-то, на что вы можете указать и сказать: «Это метапрограммирование». Я также намеренно отступаю от позиции «Нет метапрограммирования, есть только программирование». Хотя я частично согласен, это не место для таких философских дебатов. Если вам интересны эти дебаты, то есть отличный подкаст Ruby Rogues, в котором несколько выдающихся «Rubyists» обсуждают определение метапрограммирования более подробно. [1]

Начнем с того, что метапрограммирование не волшебно. Это не решит всех ваших проблем. Его использование не сделает вас рок-звездой. Я также собираюсь обойти предупреждение «Вы действительно не должны бегать с ножницами» и предположить, что вы будете применять звуковую логику в своем приложении метапрограммирования. Оставьте в стороне, это может быть прекрасным дополнением к вашему инструментальному поясу. Проще говоря, это способ заставить ваш код сделать для вас немного больше. При правильном использовании это приведет к менее повторяющемуся, более простому тестированию и более чистому коду. Поначалу это может показаться немного запутанным, но как только вы это поймете, вы сможете решить множество проблем, которые в противном случае были бы невероятно сложными. В первой части руководства я расскажу о нескольких хуках класса / модуля. Эти хуки позволят вам улучшить читаемость вашего кода. Иногда это похоже на магию, но это не так.

Если вы заблудились где-то, просто запомните правило № 1 . Кроме того, посмотрите в конце поста ссылки на дополнительные ресурсы / помощь.

Крючки

унаследованный

Первый хук, который я хотел бы обсудить, #inherited . Это хук, который позволяет такому коду существовать в Rails:


class Post < ActiveRecord::Base
has_many :comments
end

view raw
gistfile1.rb
hosted with ❤ by GitHub

Новый разработчик видит это и либо игнорирует его, либо выводит комментарий над 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

view raw
gistfile1.rb
hosted with ❤ by GitHub


Когда вы наследуете от 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»

view raw
gistfile1.rb
hosted with ❤ by GitHub

Довольно круто, имхо. Важно отметить, что #inherited вызывается до того, как подкласс полностью определен, поэтому любая условная логика, которая включает подкласс, не будет работать. Кроме того, после выполнения вашего пользовательского кода рекомендуется вызывать super after.

включены

Обратный вызов вызывается всякий раз, когда получатель включен в другой модуль или класс.

Каждый разработчик Ruby знаком с #include , которое позволяет вам внедрять методы экземпляра модуля в класс. Точно так же, чтобы включить методы класса модуля, вы должны использовать #extend . Итак, если бы у вас был модуль с методами экземпляра и класса, вам нужно было бы сделать что-то вроде следующего, чтобы получить весь модуль в вашем классе:


class MyClass
include Mymodule # instance methods
extend Mymodule # class methods
end

view raw
gistfile1.rb
hosted with ❤ by GitHub

Это подводит нас к следующему хуку, который позволяет нам выполнять произвольный код всякий раз, когда модуль входит в класс. Он определен в модуле , и с его использованием связаны некоторые идиомы. Чтобы выполнить предыдущий пример, вы можете просто определить #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 #»

view raw
gistfile1.rb
hosted with ❤ by GitHub

Это только одно использование #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

view raw
gistfile1.rb
hosted with ❤ by GitHub

Если вы выполните приведенный выше фрагмент, вы увидите, что #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»

view raw
gistfile1.rb
hosted with ❤ by GitHub

Это фактически расширит переменную 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’

view raw
gistfile1.rb
hosted with ❤ by GitHub

Выводы

Эти хуки часто используются в некоторых из самых умных частей Ruby-кода. Использование их может помочь вам написать лучший код. Кроме того, использование этих методов также поможет вам лучше понять, как Ruby выполняет / загружает / управляет каждой написанной вами программой.

В своем выступлении в 2007 году @pragdave объяснил некоторые причины, по которым расширение Ruby с помощью метапрограммирования является одной из основ языка. Он говорил о том, как метапрограммирование в Ruby поднимает уровень абстракции. По сути, это означает увеличение расстояния между машиной / языком и вашим кодом. Это хорошо по нескольким причинам. С одной стороны, код в конечном итоге выглядит гораздо более читабельным как для кодеров, так и для клиентов. Кроме того, после того, как заложен основной код, новый разработчик должен понимать меньше, чтобы иметь возможность внести свой вклад. Этот повышенный уровень абстракции является удивительной частью Ruby, и он широко представлен в некоторых из его крупнейших проектов (особенно в Rails). Я надеюсь, что вы обнаружите, что изучаете хуки, упомянутые выше, в своем собственном коде. Я уверен, что если вы это сделаете, вы начнете думать о Руби чуть более нежно. Спасибо за прочтение.

Это первое издание «Автостопом по метапрограммированию». В следующем выпуске будут рассмотрены основы метапрограммирования в Ruby. Узнайте больше о метапрограммировании в моем блоге, где я разместил несколько ссылок, которые я нашел полезными при написании этого поста. ^ _ ^