Добро пожаловать в Ruby Metaprogramming! В первой части мы рассмотрели, что такое метапрограммирование и как оно работает; мы глубоко изучили внутреннюю часть системы поиска методов Ruby и рассмотрели, как создание классов Singleton вписывается в этот механизм. Теперь о хорошей части: применяя все это.
Поддельные объекты для тестирования
Некоторые из наиболее полезных функций метапрограммирования Ruby бесчисленное количество раз демонстрировались в широком спектре доступных сред тестирования. В то время как многие из этих сред используют различные формы метапрограммирования для создания своих API, мы собираемся исследовать одно из наиболее важных применений метапрограммирования, используемых в средах тестирования Ruby: метапрограммирование для насмешек и заглушек.
Вы, вероятно, можете начать делать выводы о том, как это может работать, учитывая то, что вы теперь знаете о метапрограммировании. Давайте начнем с хорошего, простого примера:
class Book end class Borrower end
Мы собираемся притвориться, что управляем библиотекой в течение дня, и что нам нужно записать, какие книги одолжены и кем. Нам также нужно будет знать, достиг ли ли тот человек, который пытается одолжить книгу, своих лимитов. Для этого мы создаем два класса: класс Book
и класс Borrower
.
class Borrower attr_accessor :books def initialize @books = [] end end
Само собой разумеется, что Borrower
разрешено заимствовать много книг, поэтому выше мы дали каждому экземпляру Borrower
переменную класса @books
для хранения Books
они в настоящее время имеют. Мы добавили attr_accessor
чтобы мы могли получить к нему доступ в attr_accessor
и назначить его с помощью borrower.books=
. Также было бы полезно узнать, сколько книг вышло у Borrower
, поэтому давайте продолжим и добавим метод, который сделает это легко:
class Borrower # ... def books_on_loan @books.length end # ... end
Наконец, нам нужно знать, разрешено ли Borrower
больше книг (эта библиотека немного строгая, поэтому мы разрешаем людям одалживать только 5 книг одновременно); простой метод может достичь именно этого:
class Borrower # ... def can_loan? @books.length < 5 end # ... end
Теперь, поскольку вы хороший кодер, в реальном мире вы, вероятно, написали бы несколько тестов, прежде чем начали кодировать эти методы. Одна из вещей, которые вы, вероятно, начнете осознавать, делая это, чтобы проверить :can_loan?
метод, вы должны иметь коллекцию Books
назначенных Borrower
. Этого можно достичь, добавив несколько Books
к Borrower
до того, как вы протестируете реальный метод, но этот способ тестирования становится кропотливо многословным и завершится неудачей, если при создании этих экземпляров возникнут проблемы с классом Book
. Вот где на помощь приходит метапрограммирование:
describe Borrower do before :each do @borrower = Borrower.new end describe "can_loan? performs correctly" do it "returns false if equal to or over the limit" do @borrower.books.instance_eval do def length 5 end end @borrower.can_loan?.should == false end it "returns true if under the limit" do @borrower.books.instance_eval do def length 1 end end @borrower.can_loan?.should == true end end end
Я использовал RSpec здесь, но принципы будут применяться одинаково в любой среде тестирования, которую вы выберете. Благодаря возможности метапрограммирования вам больше не нужно создавать коллекцию Books
и проводить свой тест во власти внешних зависимостей; вместо этого вы можете переопределить те самые методы, которые использует ваш метод класса, чтобы заставить их возвращать значения, для которых вы проверяете. Вы можете начать осознавать всю мощь этой методологии, когда поймете, что можете заглушить даты и время, операции ввода-вывода, записи в базе данных, внешние вызовы API; список можно продолжить.
Поэтому в следующий раз, когда вы обнаружите, что создаете много внешних зависимостей или полагаетесь на дополнительные классы в своих тестах, подумайте о том, чтобы добавить в смесь немного метапрограммирования.
Прежде чем перейти к более интересному примеру, следует отметить, что насмешка и огрызание объектов — это процесс, довольно широко инкапсулированный множеством фантастических драгоценных камней. Вы найдете проекты, такие как FlexMock и RSpec Mocks, неоценимыми для того, чтобы держать ваши метапрограммы в сухих тестах сухими.
Динамические методы
Такие библиотеки, как ActiveRecord, стали известны своим интересным использованием метапрограммирования, позволяющим им создавать методы, которые генерируются на лету на основе пользовательских данных. Посмотрим, сможем ли мы реализовать нечто подобное.
Определяя метод method_missing
в классе, любые неудачные вызовы методов в этом классе или его цепочке наследования будут автоматически вызывать метод method_missing
который вы определили. В сочетании с мощью instance_eval
и class_eval
вас есть рецепт невероятных DSL и гибких API. Эта комбинация может не только использоваться для разрешения динамических методов, но также может предотвратить необходимость программирования повторяющихся методов.
В качестве примера мы попытаемся создать класс Country
который отвечает на метод is_<countryname>?
где <countryname>
— это название страны, которую вы тестируете. В качестве дальнейшего шага мы также заставим его отвечать на is_<countryname>(_or_<countryname>...)?
где _or...
может повторяться бесконечно, чтобы соответствовать списку возможных стран.
Давайте начнем с создания нашего класса Country
:
class Country attr_accessor :name def initialize(name) @name = name end end
Страна имеет атрибут name
который должен быть установлен при создании экземпляра класса. У нас также есть очень простой метод инициализатора, который дает ему начальное значение. Теперь давайте реализуем наш метод method_missing
:
class Country ... COUNTRY_QUERY_REGEX = /^is_((?:_or_)?[az]+?)+?$/i def method_missing(meth, *args, &block) if COUNTRY_QUERY_REGEX.match meth.to_s self.class.class_eval <<-end_eval def #{meth} self.__send__ :check_country, "#{meth}" end end_eval self.__send__(meth, *args, &block) else super end end end
Это выглядит намного сложнее, чем это, поэтому давайте рассмотрим это построчно. В строке 5 мы запускаем код для нашей функции method_missing
, проверяя, соответствует ли вызываемый метод регулярному выражению; не стоит слишком беспокоиться о самом Regexp
, поскольку все, что имеет значение, это то, что он соответствует вещам вида is_something<_or_somethingelse...>?
(например, is_italy?
и is_italy_or_ukraine?
).
Как только мы узнаем, что метод, который мы пытаемся вызвать, совпадают с настроенным нами шаблоном, мы можем запустить себя в уже знакомой инструкции class_eval
в строке 6. Мы используем синтаксис heredoc для эффективного создания хорошо отформатированной многострочной строки, но вы могли бы так же легко поместить весь блок кода в одну строку внутри строки. Внутри блока мы определяем метод с тем же именем, что и вызываемый, и у нас он вызывает метод, который выполняет тяжелую работу в строке 8 (которую мы определим через минуту). Мы создаем метод, который отсутствует в этом обработчике, чтобы он быстрее check_country
при последующих вызовах, но вы могли бы check_country
метод check_country
и не создавать метод, если хотите сохранить его простым. Как только мы определили это, мы вызываем метод, который мы только что создали.
Если шаблон не соответствовал при method_missing
, мы вызываем super
в строке 13; это важно, так как если вы здесь не вызываете super
никакие другие классы в цепочке наследования не будут спрашиваться, могут ли они ответить на отсутствующий метод.
Теперь, чтобы определить содержимое нашего метода check_country
:
class Country ... private def check_country(query) countries = query[3..-2].split("_or_") countries.any? { |s| s == @name } end end
Внутри этого метода мы разбиваем запрос на Array
, разделяя их между битами « или »; при этом будет использоваться все значение query
если оно не содержит ни «, ни «. Раз это Array
, мы используем метод Enumerable
any?
проверить, соответствует ли какой-либо из элементов в Array
названию страны, которую представляет наш экземпляр класса. Поскольку это последний вызов метода, его возвращаемое значение также является возвращаемым значением функции.
Теперь давайте попробуем:
italy = Country.new("italy") italy.is_ukraine? # => false italy.is_italy? # => true italy.is_ukraine_or_italy? # => true italy.is_ukraine_or_australia_or_portugal_or_italy? # => true
К настоящему времени вы, вероятно, осознали всю мощь того, что мы создали. Создание этих методов вручную было бы либо чрезвычайно трудоемким, либо сложным в обслуживании, либо невозможным. Комбинируя методы метапрограммирования с нашим method_missing
вызовом method_missing
, мы создали чрезвычайно выразительный и красивый API, который является СУХИМЫМ и простым в обслуживании.
Вывод
Хотя это выглядит сложным, в этой статье мы добились того, что часто было бы очень трудно или затруднительно нормально кодировать, и в этом вся прелесть метапрограммирования. Вещи, которые могут казаться трудными или даже невозможными, часто могут быть закодированы с использованием простых методов метапрограммирования.
Вы узнали о классе Singleton в Ruby и о том, как он изменяет саму структуру языка, добавляя новый уровень поиска для каждого вызова метода, и мы применили эту мощную технику для создания кода, который может превратить повседневные решения в элегантные и повторно используемые. узоры.
Дальнейшее чтение
- Узнайте больше о синглете Ruby с Питером Джонсом из Contextual Development.
- Получите Кирку метапрограммирования, «Рубин метапрограммирования» , написанный Паоло Перроттой.
- У Дэйва Томаса есть великолепная серия видеороликов о PragProg под названием «Объектная модель Ruby и метапрограммирование» .