Статьи

Рубин Метапрограммирование: Часть II

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

Дальнейшее чтение