Dsl что такое в ruby
Каковы некоторые определяющие характеристики Ruby DSL, которые отделяют его от обычного API?
Когда вы используете API, вы создаете экземпляры объектов и вызываете методы в обязательном порядке. С другой стороны, хороший DSL должен быть декларативным, представлять правила и отношения в вашей проблемной области, а не инструкции, которые необходимо выполнить. Более того, в идеале DSL должен быть читаемым и изменяемым кем-то, кто не является программистом (что не относится к API).
Также имейте в виду различие между внутренними и внешними DSL.
- Внутренний предметный язык встроен в язык программирования (например, Ruby). Его легко реализовать, но структура DSL зависит от родительского языка, в который он встроен.
- Язык, зависящий от внешнего домена - это отдельный язык, разработанный с учетом конкретной предметной области. Это дает вам большую гибкость, когда дело доходит до синтаксиса, но вы должны реализовать код для его интерпретации. Это также более безопасно, поскольку лицо, редактирующее правила домена, не имеет доступа ко всем возможностям родительского языка.
DSL (предметно-ориентированный язык) - это чрезмерно разрекламированный термин. Если вы просто используете подмножество языка (скажем, Ruby), чем он отличается от исходного? Ответ - нет.
Однако, если вы выполните некоторую предварительную обработку исходного текста, чтобы ввести новый синтаксис или новую семантику, которых нет в основном языке, тогда у вас действительно есть новый язык, который может быть предметно-зависимым.
Комбинация поэтического режима Ruby и перегрузки операторов действительно дает возможность получить что-то, что одновременно является легальным синтаксисом Ruby и разумным DSL.
И продолжающееся ухудшение, связанное с XML, действительно показывает, что, возможно, простой DSL, встроенный во все эти файлы конфигурации, не был полностью ошибочным .
Добавление новых методов в класс Object, чтобы вы могли просто вызывать их, как если бы они были встроенными языковыми конструкциями. (см. грабли)
Создание методов для настраиваемого объекта или набора объектов, а затем использование файлов сценариев для выполнения операторов в контексте объекта верхнего уровня. (см. capistrano)
Создание методов для настраиваемого объекта или набора объектов, чтобы пользователь создал объект для использования этих методов.
Создание методов как методов класса, чтобы пользователь ставил имя класса перед всеми методами.
Создание методов в виде миксина, который пользователи включают или расширяют для использования методов в своих настраиваемых объектах.
Так что да, грань между ними тонкая. Превратить настраиваемый набор объектов в DSL просто, добавив один метод, запускающий файл сценария в правильном контексте.
Для меня разница между DSL и API заключается в том, что DSL может быть, по крайней мере, понят (и проверен), если он не написан как подязык Ruby кем-то в этой области.
Например, у вас могут быть финансовые аналитики, пишущие правила для приложения для торговли акциями на Ruby DSL, и им никогда не придется знать, что они используют Ruby.
По сути, это одно и то же. DSL обычно реализуются с помощью обычных языковых механизмов в Ruby, поэтому технически они все API.
Однако для того, чтобы люди распознали что-либо как DSL, это обычно заканчивается добавлением того, что выглядит как декларативные утверждения к существующим классам. Что-то вроде валидаторов и объявлений отношений в ActiveRecord.
Выглядит как DSL, в то время как следующее - нет:
Оба они будут реализованы обычным кодом Ruby. Просто один выглядит так, будто у вас есть классные новые языковые конструкции, а другой кажется довольно заурядным (и излишне многословным и т. Д.).
В этой статье я проиллюстрирую основные возможности Ruby для построения Domain Specific Languages(DSL). DSL, это небольшие, узкоспециализированные языки для решения конкретных задач. В отличие от языков общего назначения, таких как C++ или Java, DSL обычно очень компактны, и обладают высокой выразительностью в контексте решаемой задачи.
Различные DSL широко распространены в библиотеках и фреймворках для Ruby. Например в Rails DSL используются для создания миграций.
А теперь, давайте посмотрим какие возможности Ruby предоставляет для построения DSL
Пусть нам нужен простой формат для описания комплектации компьютера.
Простой пример:
Теперь с помощью Ruby построим удобный DSL для таких описаний.
Этап 1.
Трансформируем даное описание в Ruby код, например так(пусть память мы храним в мегабайтах а частоту в мегагерцах)
Код класса элементарный:
В Ruby все переменные экземпляра(такие переменные начинаются с @) являются приватными, т.е. доступны только внутри методов объекта. Чтобы сделать аттрибут, мы должны объявить два метода для установки и получения значения этого аттрибута:
или проще, вызвать: attr_accessor :cpu , который и сгенерирует нам эти методы
Этап 2.
Ну и что? ничего нового тут нет, просто используем объект с аттрибутами. Попробуем немного усовершенствовать наш код.
Первое что бросается в глаза, мы должны самостоятельно переводить гигабайты в мегабайты и тд. Исправим!
Для этого примешаем методы ghz и gb к классу Numeric
Еще я добавил два метода mhz и mb, cpu.ram = 512.mb вместо cpu.ram = 512.
В Ruby есть возможность примешать(mixin) новый метод к любому классу. Т.е. мы можем расширять класс даже после его создания. После того как мы примешали метод к классу, он становится доступным для всех его экземпляров.
В методе cool self — это указатель на значения самого объекта, а так как возвращаемое значение метода это результат выполнения его последней строки, то
puts "my string".cool
выведет на экран «My string is cool!»
Методы ghz и тд я примешал к классу Numeric, потому что это родительский класс для всех чисел в Ruby. И для целых и для дробных
Этап 3.
Уже лучше. Но еще смущает тот факт, что перед каждым параметром мы должны указывать «comp.». Сделаем немного подругому:
Выглядит намного лучше, не правда ли? Но вопрос будет ли это работать?
Давайте разберемся. На вид, это валидный Ruby код. cpu, ram и disk это уже не методы, а функции, так как вызываются не у экземпляра класса Computer.
Что то наподобии этого:
Но как нам передать в эту функцию переменную comp?
cpu 2.ghz, comp? но тогда потяряется вся выразительность. Вот если бы мы могли выполнить эти методы в контексте этого объекта…
И ведь мы можем! Ruby дает нам такую возможность c помощью метода instance_eval.
Теперь посмотрим на новую реализацию класса Computer
Что же делает этот метод? Все очень просто. Как уже сказал выше, он выполняет блок(а можно и строку с кодом) в контексте данного объекта.
А так как в классе Computer объявлены методы cpu и тд, то они и вызовутся. И именно для этого объекта.
Этап 4.
Теперь представим, что у нашего компьютера неограниченно много различных характеристик. Например, размер BIOS'a или разрадность шины:
Все мы предугадать не можем, но хочется чтобы была возможность добавления таких характеристик.
И тут нам снова приходят на помощь возможности Ruby, а именно метод method_missing.
method_missing специальный метод объекта, который вызывается при попытке вызвать несуществующий метод. Пример:
Теперь вернемся к классу Computer:
Теперь как это работает. Мы передаем блок конструктору нашего класса. Этот блок выполняется в контексте экземпляра этого класса. В процессе выполнения вызываются несуществующие методы, вместо них выполняется method_missing с параметром name равным имени метода и массивом аргументов args . Теперь мы создаем переменную экземпляра с именем, совпадающим с методом, и значением, равным первому аттрибуту вызванного метода. А также метод для получения значения этой переменной.
Кстати, method_missing используется в Rails в ActiveRecord, для создание тысяч методов типо Person.find_all_by_name
Получившийся код можно посмотреть тут.
Дальнейшие усовершенствования
Что еще можно придумать, чтобы наш DSL стал еще удобнее?
Так как DSL могут быть предназначены не только для программистов, но и для людей незнакомых с программированием, то логично вынести описания в файл, который может редактировать даже не программист. А потом загружать и интерпретировать этот файл.
my_pc.conf:
cpu 1.8.mgh
ram 512.mb
disk 40.gb
Сделать это очень просто, как я уже писал выше, методу instance_eval можно передать строку с кодом вместо блока.
Во-вторых, для фанатов русского языка, можно писать так:
comp = Computer.new do
процессор 2.2.ghz
память 2.gb
диск 1.gb
end
Чтобы это сработало, надо просто добавить параметр -Ku при вызове интерпретатора. Например так:
ruby -Ku test.rb
Выше мы построили простой DSL, который является валидным Ruby кодом. Но можно отказаться от валидности. Например избавиться от точки.
Вместо cpu 1.ghz писать cpu 1ghz . Тогда придется произвести небольшой препроцессинг. Добавить эти точки, например с помощью регулярных выражений.
А теперь, если мы скомбинируем эти улучшения, то мы сможем проинтерпритировать пример, который я дал в самом начале:
Представленный текст является переводом статьи из официального блога компании ZenPayroll. Несмотря на то, что в некоторых вопросах я не согласен с автором, общий подход и методы, показанные в этой статье, могут быть полезны широкому кругу людей, пишущих на Ruby. Заранее извиняюсь за то, что некоторые бюрократические термины могли быть переведены некорректно. Здесь и далее курсивом выделены мои примечания и комментарии.
В ZenPayroll мы стараемся максимально скрыть сложность решаемой задачи. Начисление заработной платы традиционно было бюрократическим осиным гнездом, и реализация современного и удобного решения в столь недружелюбной атмосфере — это привлекательная техническая задача, которую очень сложно решить без
автоматизации.
ZenPayroll сейчас создает общегосударственный сервис (реализован уже в 24 штатах), что означает, что мы удовлетворяем множеству требований, уникальных для каждого штата. Поначалу мы заметили, что тратим много времени на написание шаблонного кода вместо того, чтобы сконцентрироваться на том, что делает каждый
штат уникальным. Вскоре мы поняли, что эту проблему мы можем решить, используя преимущества создания DSL , чтобы ускорить и упростить процесс разработки.
В этой статье мы создадим DSL, максимально близкий к тому, что мы используем сами.
Когда нам нужен DSL?
Написание DSL — это огромное количество работы, и оно далеко не всегда может помочь вам в решении задачи. В нашем случае, однако, достоинства перевесили недостатки:
- Весь специфичный код собран в одном месте.
В нашем Rails-приложении есть несколько моделей, в которых мы должны реализовать специфичный для каждого штата код. Нам нужно генерировать формы, таблицы и манипулировать обязательной информацией, имеющей отношение к сотрудникам, компаниям, графикам подачи документов и ставкам налогов. Мы проводим платежи государственным структурам, подаем сгенерированные формы, вычисляем подоходный налог и многое другое. Реализация DSL позволяет нам собрать весь код, специфичный для шатата, в одном месте. - Шаблонизация штатов.
Вместо того, чтобы создавать с нуля каждый новый штат, использование DSL позволяет нам автоматизировать создание общих для штатов вещей и, в то же время, позволяет гибко настраивать каждый штат. - Уменьшение количества мест, где можно ошибиться.
Имея DSL, создающий для нас классы и методы, мы сокращаем шаблонный код и имеем меньше мест, куда вмешиваются разработчики. Качественно оттестировав DSL и защитив его от неправильных входных данных, мы очень сильно снижем вероятность возникновения ошибки. - Возможность быстрого расширения.
Мы создаем фреймворк, который облегчает реализацию уникальных требований для новых штатов. DSL это набор инструментов, сохраняющий нам время на это и позволяющий разработке двигаться дальше.
Написание DSL
В рамках этой статьи мы сконцентрируемся на создании DSL, который позволит нам хранить идентификационные номера компаний и параметры начисления зарплаты (использующиеся для вычисления налогов). Хотя это всего лишь беглый взгляд на то, что может предоставить нам DSL, это все-еще полноценное введение в тему. Наш итоговый код, написанный с помощью созданного DSL, будет выглядеть примерно так:
Отлично! Это чистый, понятный и выразительный код, использующий интерфейс, разработанный для решения нашей задачи. Давайте начнем.
Определение параметров
В первую очередь, давайте определимся, что мы хотим получить в итоге. Первый вопрос: какую информацию мы хотим хранить?
Каждый штат требует от компаний регистрироваться у местных властей. При регистрации в большинстве штатов, компаниям выдаются идентификационные номера, которые требуются для выплаты налогов и подачи документов. На уровне компании мы должны иметь возможность хранить различные идентификационные номера для разных штатов.
Удерживаемые налоги рассчитываются исходя из количества пособий, получаемых сотрудником. Это величины, которые определяются в формах W-4 для каждого штата. Для каждого штата есть множество вопросов, задающихся, чтобы определить ставки налогов: ваш статус налогоплательщика, связанные льготы, пособия по инвалидности и многое другое. Для сотрудников нам нужен гибкий метод для определения различных атрибутов для каждого штата, чтобы правильно считать налоговые ставки.
DSL, который мы напишем, будет обрабатывать идентификационные номера компаний и базовую информацию о начислении зарплаты для сотрудников. Дальше мы используем этот инструмент для описания Калифорнии. Так как Калифорния имеет некоторые дополнительные условия, которые необходимо учитывать при рассчете зарплаты, мы сконцентрируемся на них для того, чтобы показать, как разрабатывать DSL.
Я предоставляю ссылку на простое Rails-приложение для того, чтобы вы могли следовать шагам, которые будут сделаны в этой статье.
В приложении используются следующие модели:
- Company. Описывает сущность «компания». Хранит информацию о названии, типе и дате основания.
- Employee. Описывает сотрудника, работающего на компанию. Хранит информацию об имени, платежах и дате поступления на работу.
- CompanyStateField. Каждая компания связана со многими CompanyStateField, каждый из которых хранит определенную информацию, связанную с компанией и специфичную для штата, например, идентификационный номер. В калифорнии от работодателя требуются два номера: номер в департаменте развития занятости (EDD) и номер в секретариате штата (SoS). Больше информации по этому вопросу можно найти здесь.
- EmployeeStateField. Каждый сотрудник связан со многими EmployeeStateField, каждый из которых хранит информацию сотрудника, специфичную для штата. Это информация, которую можно найти в формах W-4 штата, например, скидки при удержании налогов или статус налогоплательщика. Калифорнийская форма DE4 требует указания налоговых скидок, удерживаемой суммы в долларах, и статуса налогоплательщика (холост, женат, глава семьи).
Мы создаем модели-наследники от моделей CompanyStateField и EmployeeStateField, которые будут использовать те же таблицы, что и базовые классы (single table inheritance). Это позволяет нам определять их наследников, специфичных для штата, и использовать только одну таблицу для хранения данных всех таких моделей. Чтобы это осуществить, обе таблицы содержат сериализованные хеши, которые мы и будем использовать для хранения специфичных данных. Хотя по этим данным и нельзя будет проводить запросы, это позволяет нам не раздувать базу неиспользуемыми столбцами.
Прим. переводчика. При использовании Postgres, эти данные можно хранить в нативно поддерживаемом JSON.
Наше приложение подготовлено для работы со штатами, и теперь наш DSL должен создавать специфичные классы, которые и реализуют требуемую функциональность для Калифорнии.
Что нам поможет?
Метапрограммирование — это та область, где Ruby может показать себя во всей красе. Мы можем создавать методы и классы прямо во время выполнения программы, а также использовать огромное количество методов метопрограммирования, что превращает создание DSL на Ruby в сплошное удовольствие. Сам по себе Rails это
DSL для создания web-приложений и огромное количество его «магии» базируется на возможностях метапрограммирования Ruby. Ниже я приведу небольшой список методов и объектов, которые будут полезны для метапрограммирования.
Блоки
Блоки позволяют нам группировать код и передавать его в виде аргумента для метода. Их можно описывать с помощью конструкции do end или фигурных скобок. Оба варианта тождественны.
Прим. переводчика. Согласно принятому стилю, синтаксис do end используется в многострочных конструкциях, а фигурные скобки — в однострочных.
Практически наверняка вы их использовали, если пользовались методом типа each:
Это прекрасная вещь для создания DSL, потому что они позволяют нам создать код в одном контексте, а выполнить его в другом. Это дает нам возможность создать читабельный DSL, вынося определения методов в другие классы. Много примеров этого мы увидим далее.
Метод send позволяет нам вызывать методы объекта (даже приватные), передавая ему имя метода в виде символа. Это полезно для вызова методов, которые обычно вызываются внутри определения класса или для интерполяции переменных для динамических вызовов метода.
define_method
В Ruby define_method дает нам возможность создавать методы не используя обычную процедуру при описании класса. Он принимает в качестве аргументов строку, которая будет именем метода и блок, который будет выполняться при вызове метода.
instance_eval
Это вещь, необходимая при создании DSL почти так же, как и блоки. Он принимает блок и выполняет его в контексте объекта-приемника. Например:
В этом примере блок содержит вызов метода say_hello, несмотря на то, что в его контексте такого метода нет. Экземпляр класса, возвращенный из MyClass.new, является приемником для instance_eval и вызов say_hello происходит в его контексте.
Мы снова описываем блок, который вызывает неопределенный в его контексте метод. В этот раз мы передаем блок в конструктор класса MyOtherClass и выполняем его в контексте self приемника, который является экземпляром MyOtherClass. Отлично!
method_missing
Это та магия, благодаря которой работают методы find_by_* в Rails. Любой вызов неопределенного метода попадает в method_missing, который принимает на вход имя вызванного метода и все переданные ему аргументы. Это еще одна прекрасная вещь для DSL, потому что она позволяет создавать методы динамически, когда мы не знаем, что может быть реально вызвано. Это дает нам возможность создать очень гибкий синтаксис.
Проектирование и реализация DSL
Теперь, когда у нас есть некоторые знания о нашем наборе инструментов, пришло время подумать о том, каким мы хотим видеть наш DSL и как с ним будут дальше работать. В данном случае, мы будем работать «задом наперед»: вместо того, чтобы начинать с создания классов и методов, мы разработаем идеальный синтаксис и будем строить все остальное вокруг него. Будем считать этот синтаксис эскизом того, что мы хотим получить. Давайте снова взглянем на то, как все должно выглядеть в итоге:
Давайте разобьем это на части и будем постепенно писать код, который облачит наш DSL в классы и методы, которые нам нужны, чтобы описать Калифорнию.
Если вы хотите следовать за мной с помощью предоставленного кода, то можете сделать git checkout step-0 и дописывать код вместе со мной в процессе чтения.
Наш DSL, который мы назвали StateBuilder — это класс. Мы начинаем создание каждого штата с вызова метода класса build с аббревиатурой имени штата и описывающего его блока в качестве параметров. В этом блоке, мы можем вызывать методы, которые мы назовем company и employee и передавать каждому из них собственный конфигурационный блок, который будет настраивать наши специализированные модели (CompanyStateField::CA и EmployeeStateField::CA)
Как было упомянуто ранее, наша логика инкапсулирована в класс StateBuilder. Мы вызываем блок, переданный в self.build в контексте нового экземпляра StateBuilder, поэтому company и employee должны быть определены и каждый из них должен принимать блок в качестве аргумента. Давайте начинем разработку с создания болванки класса, которая подходит под эти условия.
Теперь у нас есть база для нашего StateBuilder. Так как методы company и employee будут определять классы CompanyStateField::CA и EmployeeStateField::CA, давайте определимся, как должны будут выглядеть блоки, которые мы будем передавать этим методам. Мы должны определить каждый атрибут, который будут иметь наши модели, а также некоторую информацию об этих атрибутах. Что особенно приятно в создании собственного DSL, так это то, что мы не обязаны использовать стандартный синтаксис Rails для методов-геттеров и сеттеров, а также валидаций. Вместо этого, давайте реализуем синтаксис, который мы описывали ранее.
Прим. переводчика. Спорная мысль. Я бы все-таки постарался минимизировать зоопарк синтаксисов в рамках приложения, пусть и за счет некоторой избыточности кода.
Пришло время сделать git checkout step-1.
Для калифорнийских компаний мы должны хранить два идентификационных номера: номер выданный Калифорнийским Департаментом Занятости (EDD) и номер, выданный секретариатом штата (SoS).
В идеале, мы должны использовать имя нашего атрибута в качестве имени метода, которому в качестве параметра передавать блок, который определит формат этого поля (Кажется, пришло время для method_missing!).
Прим. переводчика. Возможно, со мной что-то не так, но синтаксис вида
мне кажется более понятным и логичным, чем предложенный автором (сравните со стандартными миграциями). При использовании авторского синтаксиса с первого взгляда вовсе не очевидно, что в блоках, описывающих компанию или работника, допустимо писать любые имена, а также вы получаете прекрасный гранатомет для стрельбы в ногу (см. далее).
Давайте напишем, как будут выглядеть вызовы этих методов для номеров EDD и SoS.
Обратите внимание, что здесь при описании блока мы сменили синтаксис с do end на фигурные скобки, но результат при этом не изменился — мы все так же передаем исполняемый блок кода в функцию. Теперь давайте проведем аналогичную процедуру и для сотрудников.
Согласно калифорнийскому свидетельству о льготах при начислении налогов, работников спрашивают о их статусе налогоплательщика, количестве льгот и любых других дополнительных удерживаемых суммах, которые у них могут быть. Статусом налогоплательщика может быть Одинок, Состоит в браке или Глава Семьи; налоговые льготы не должны превышать 99, а для дополнительных удерживаемых сумм давайте установим максимум в $10,000. Теперь давайте опишем их так же, как сделали это для полей компании.
Теперь у нас есть окончательная реализация для Калифорнии. Наш DSL описывает атрибуты и валидации для CompanyStateField::CA и EmployeeStateField::CA с использованием нашего собтвенного синтаксиса.
Все, что нам осталось — это перевести наш синтаксис в классы, геттеры/сеттеры и валидации. Давайте реализуем методы company и employee в классе StateBuilder и получим работающий код.
Третья часть марлезонского балета: git checkout step-2
Назовем наши контейнеры StateBuilder::CompanyScope и StateBuilder::EmployeeScope и создадим в StateBuilder методы, создающие экземпляры этих классов.
Мы используем const_set для того, чтобы определить подклассы CompanyStateField и EmployeeStateField с именем нашего штата. Это создаст нам классы CompanyStateField::CA и EmployeeStateField::CA, каждый из которых наследуется от соответствующего родителя.
Теперь мы можем сосредоточиться на последнем этапе: блоках, переданных каждому из наших создаваемых атрибутов (sos, edd, additional_witholding и т.д.). Они будут выполнены в контексте CompanyScope и EmployeeScope, но если мы попробуем сейчас выполнить наш код, то получим ошибки о вызове неизвестных методов.
Воспользуемся методом method_missing чтобы обработать эти случаи. В текущем состоянии мы можем полагать, что любой вызванный метод является именем атрибута, а блоки, переданные в них, описывают то, как мы хотим его сконфигурировать. Это дает нам «магическую» возможность определять нужные атрибуты и сохранять их
в базу данных.
Внимание! Использование method_missing так, что не предусмотрено ситуации, когда может быть вызван super, может привести к неожиданному поведению. Опечатки будет трудно отслеживать, так как все они будут попадать в method_missing. Убедитесь, что созданы варианты, при которых method_missing вызовет super, когда будете писать что-то, основываясь на этих принципах.
Прим. переводчика. Вообще, лучше свести использование method_missing к минимуму, потому что оно очень сильно замедляет программу. В данном случае это не критично, так как весь этот код выполняется только при старте приложения
Определим метод method_missing и передадим эти аргументы в последний контейнер, который мы создадим — AttributesScope. Этот контейнер будет вызывать store_accessor и создавать валидации, основываясь на тех блоках, которые мы ему передадим.
Теперь каждый раз, когда мы будем вызывать метод в блоке company в app/states/ca.rb, он будет попадать в определенную нами функцию method_missing. Первым ее аргументом будет имя вызванного метода, оно же имя определяемого атрибута. Мы создаем новый экземпляр AttributesScope, передавая ему класс, который будем изменять, имя определяемого атрибута и блок, конфигурирующий атрибут. В AttributesScope мы будем вызывать store_accessor, который определит геттеры и сеттеры для атрибута, и использовать сериализованный хеш для хранения данных.
Также нам надо определить методы, которые мы вызываем внутри блоков, конфигурирующих атрибуты (format, max, options) и превратить их в валидаторы. Мы сделаем это, преобразовывая вызовы этих методов в вызовы валидаций, которые ожидает Rails.
Наш DSL готов к бою. Мы успешно определили модель CompanyStateField::CA, которая хранит и валидирует номера EDD и SoS, а также модель EmployeeStateField::CA, которая хранит и валидирует налоговые льготы, статус налогоплательщика и дополнительные сборы для сотрудников. несмотре на то, что наш DSL был создан
для автоматизации достаточно простых вещей, каждый из его компонентов может быть легко расширен. Мы можем легко добавить новые хуки в DSL, определить больше методов в моделях и развивать его дальше, основываясь на функционале, который мы реализовали сейчас.
Наша реализация заметно уменьшает повторения и шаблонный код в бэкэнде, но все-еще требует, чтобы у каждого штата были собственные представления (views) на стороне клиента. Мы расширили нашу внутреннюю разработку, чтобы она охватывала и клиентскую часть для новых штатов, и если в комментариях будет проявлен интерес, я напишу еще один пост, рассказывающий о том, как это работает у нас.
Эта статья показывает только часть того, как мы используем наш собственный DSL в качестве инструмента для расширения штатов. Подобные инструменты доказали потрясающую полезность в расширении нашего зарплатного сервиса на оставшуюся часть США, и если подобные задачи вас интересуют, то мы можем работать вместе!
Трудно найти хоть одного рубиста, ни разу не использовавшего какой-либо DSL: Sinatra, RSpec, ActiveModel — все они так или иначе являются примерами сей замечательной штуки.
Для лучшего понимания вещей, которые лежат в их основе, мы с вами склепаем небольшой класс, позволяющий объявлять хеши вот таким образом:
Объявим DSLHash, а в нем пустой статический метод build:
В дальнейшем этот метод будет возвращать нам готовый хеш.
Для начала определим в build, действительно ли нам скормили блок. Для этого в рубях есть метод “block_given?” Изменим наш build следующим образом:
Почему не “yield”? Про это ниже.
Возможно , вы видели возникающий, казалось бы, из ниоткуда params в Rails или Sinatra, содержащий параметры запроса, но явно в функцию не передаваемый. Что за магия? А магия эта именуется instance_eval
instance_eval позволяет нам запускать код в контексте определенного инстанса класса. В этом и есть главное отличие от yield, запускающего блок в текущем контексте.
Добавим вложенный класс в DSLHash:
В нем мы объявляем пустой хеш и геттер для него.
Посмотрим на наш пример и подумаем, чего же нам не хватает. Например, до сих пор не ясно, как first_name “Andrey” превратится в :first_name => “Andrey” внутри хеша.
Тут нам поможет своеобразный коллбек method_missing, который нужно задать в нашем классе-контексте. Каждый раз, когда мы вызываем несуществующий метод, Ruby будет вызвать method_missing. Конечно, если он есть.
Добавим его в наш DSLHashContext:
В name нам заботливо прилетит символ (для тех, кто не знает что эта штука называется символами, выглядит оно так: :first_name), равный имени вызванного метода.
В args нас будут ждать аргументы, а в block — соответственно, блок.
И так, теперь каждый раз, когда внутри DSLHashContext мы будем вызывать несуществующий метод, в консоль будет писаться его имя.
Теперь можно перейти и к instance_eval. Изменим наш метод build следующим образом:
Теперь мы можем попробовать запустить пример из начала статьи. И увидим мы вот что:
Прикольно, но мы не видим того, что прячется внутри social. Немного изменим method_missing:
И вот, совсем другое дело:
Про method_missing, block_given? и instance_eval я вам уже рассказал, осталось из всего что у нас есть “добить” наш DSLHash до запланированного функционала.
Вся магия будет происходить внутри method_missing, вот так он будет выглядеть после всех метаморфоз:
Читайте также: