Если вы до сих пор не видели и не писали XML, считайте себя очень счастливым.
XML начинался как очень дружественный и разумный способ представления данных, чтобы они были удобочитаемыми и машиночитаемыми одновременно. Конечно, это звучало чертовски здорово, пока люди не начали создавать невероятно большие системы (и, возможно, сверхинженерные системы) на основе XML. Этот сайт является приятным местом для программистов всех уровней, поэтому мы не будем упоминать какие-либо конкретные технологии … но, вы знаете, кто вы есть;)
XML не так уж и ужасен — он имеет большой смысл для некоторых приложений. Но вы не можете обойти тот факт, что XML иногда раздражает, и некоторые приложения действительно нуждаются в большей гибкости, чем XML.
DSL (предметно-ориентированный язык) — это небольшой, специализированный язык, который предназначен для выполнения нескольких задач, но делает их хорошо. Примером может служить Gemfile «DSL», который поставляется с Rails. Фактически, отсутствие XML было одной из самых больших приманок в Rails (для меня и, скорее всего, для многих других).
Дело в том, что XML — это только данные. Это не язык программирования; Таким образом, вы не можете выразить какие-либо вычисления с ним. И это не самый читаемый во многих случаях.
Веб-фреймворки — это не единственное место, где возникает ситуация, когда XML (или любой другой подобный формат обработки данных) не имеет особого смысла. Vim и Emacs имеют специфичные для домена языки. Как правило, к финансовым приложениям обычно прикрепляются какие-то DSL. Дело в том, что DSL обладает мощью, которую не может предложить ни один формат представления данных.
Это не значит, что DSL нельзя использовать для представления данных! Они уверены, что могут, и, на самом деле, при правильной разработке они работают лучше, чем многие другие традиционные системы, такие как XML или JSON.
В прежние времена DSL было не так легко написать. Вы должны были определить язык, используя формальный метод, сгенерировать лексер и парсер и, наконец, скомпилировать или интерпретировать язык. Это было действительно много работы, потому что вы написали WRITE A FREAKING COMPILER.
Но с динамическими языками жизнь намного проще, а DSL действительно набирает популярность. Основная идея, вместо того, чтобы писать
компилятор, мы используем возможности самого Ruby, чтобы помочь в разработке языка. Мы определили несколько методов, и, используя эти методы, пользователь DSL может достичь своей цели, используя Ruby. Этот вид программирования часто называют « метапрограммированием », поскольку он манипулирует другим кодом.
Особая идея заключается в том, что если вы действительно не хотите этого, вы полностью скрыты от Ruby. Это не очень похоже на Ruby:
forward 1 | |
left 16 | |
forward 5 | |
right 6 | |
backward 3 |
Но это очень простой DSL, который мы можем использовать для определения Ruby.
До сих пор мы только что говорили об этом. Давайте продолжим создавать DSL с Ruby!
Определение
Прежде всего, нам нужно понять, что именно мы хотим от языка и как должен выглядеть DSL.
Давайте представим робота; он может двигаться только вперед, назад, вправо и влево на некоторое количество единиц. Мы должны отслеживать его координаты по отношению к точке, с которой он начал, которую мы будем называть началом координат. Он также может стрелять, и мы должны отслеживать координаты, по которым стреляет робот.
Это довольно четкое описание. Это может быть что-то, что используется для веб-сайта, на котором проводятся соревнования для таких роботов (загрузите ваш код, и ваш робот будет противостоять другим). Было бы весело, если бы в DSL было еще несколько функций! Но мы пойдем с KISS сейчас.
Теперь нам нужно иметь пример кода, чтобы пойти с этим:
forward 1 | |
fire | |
backward 2 | |
fire | |
right 16 | |
fire | |
backward 2 | |
fire | |
left 2 |
В порядке, что это хорошо. Это ясно показывает, как мы хотим, чтобы наш язык выглядел. Мы просто должны это реализовать.
Реализация
Реализация DSL невероятно проста, на самом деле настолько проста, что вам захочется делать это постоянно!
Мы загружаем немного кода из файла и используем специальный маленький метод с именем «instance_eval», чтобы запустить его как код Ruby, но с некоторыми специальными методами. В нашем DSL мы хотим определить методы «вперед», «назад», «вправо», «влево» и «огонь».
Для этого мы определяем класс (вы можете называть его как угодно, я буду называть его роботом):
class Robot | |
end |
Добавьте в методы:
class Robot | |
def forward | |
end | |
def backward | |
end | |
def right | |
end | |
def left | |
end | |
end |
Теперь, в качестве практической демонстрации, мы добавляем код, который выводит имя метода с именем:
Теперь мы используем instance_eval. Его можно использовать с любым (в значительной степени) любым объектом, и он принимает блок в качестве аргумента. Вот:
class Robot | |
def forward | |
puts «forward» | |
end | |
def backward | |
puts «backward» | |
end | |
def right | |
puts «right» | |
end | |
def left | |
puts «left» | |
end | |
end | |
Robot.new.instance_eval do | |
forward | |
backward | |
left | |
right | |
right | |
end |
Это должно распечатать порядок методов при запуске. Почему это круто? Потому что это показывает нам, что небо это предел!
Поскольку мы можем определить, какого черта мы хотим в классе Robot, создание DSL становится очень простым и требует большого творческого потенциала.
Помните, мы хотим отслеживать координаты робота и координаты, по которым мы стреляли. Давайте сначала займемся первым.
Мы начинаем с добавления в конструктор (и избавляемся от «пут» в методах, которые были только в качестве примера):
class Robot | |
def initialize | |
@coordinates = [0, 0] | |
@fired_at = [] | |
end | |
def forward | |
end | |
def backward | |
end | |
def right | |
end | |
def left | |
end | |
end |
Мы добавили необходимые структуры данных в качестве переменных объекта, чтобы отслеживать информацию о нашем любимом роботе.
Теперь мы заполним методы перемещения, чтобы они фактически изменили информацию, которую мы отслеживаем:
class Robot | |
def initialize | |
@coordinates = [0, 0] | |
@fired_at = [] | |
end | |
def forward(n) | |
@coordinates[0] += n | |
end | |
def backward(n) | |
@coordinates[0] -= n | |
end | |
def right(n) | |
@coordinates[1] += n | |
end | |
def left(n) | |
@coordinates[1] -= n | |
end | |
end |
Мы добавляем метод «огонь» и логику, необходимую для хранения местоположений fired_at:
class Robot | |
def initialize | |
@coordinates = [0, 0] | |
@fired_at = [] | |
end | |
def forward(n) | |
@coordinates[0] += n | |
end | |
def backward(n) | |
@coordinates[0] -= n | |
end | |
def right(n) | |
@coordinates[1] += n | |
end | |
def left(n) | |
@coordinates[1] -= n | |
end | |
def fire | |
@fired_at.push(@coordinates) | |
end | |
end |
Перед тестированием мы должны добавить некоторые «путы», чтобы мы могли реально увидеть, что происходит:
class Robot | |
def initialize | |
@coordinates = [0, 0] | |
@fired_at = [] | |
end | |
def forward(n) | |
@coordinates[0] += n | |
puts @coordinates | |
end | |
def backward(n) | |
@coordinates[0] -= n | |
puts @coordinates | |
end | |
def right(n) | |
@coordinates[1] += n | |
puts @coordinates | |
end | |
def left(n) | |
@coordinates[1] -= n | |
puts @coordinates | |
end | |
def fire | |
@fired_at.push(@coordinates) | |
puts @coordinates | |
end | |
end | |
Наконец, мы добавляем instance_eval обратно с небольшим количеством тестового кода, чтобы увидеть, находится ли то, что мы разработали, на правильном пути:
class Robot | |
def initialize | |
@coordinates = [0, 0] | |
@fired_at = [] | |
end | |
def forward(n) | |
@coordinates[0] += n | |
puts @coordinates, «——« | |
end | |
def backward(n) | |
@coordinates[0] -= n | |
puts @coordinates, «——« | |
end | |
def right(n) | |
@coordinates[1] += n | |
puts @coordinates, «——« | |
end | |
def left(n) | |
@coordinates[1] -= n | |
puts @coordinates, «——« | |
end | |
def fire | |
@fired_at.push(@coordinates) | |
puts @coordinates | |
puts @fired_at, «——« | |
end | |
end | |
Robot.new.instance_eval do | |
backward 1 | |
forward 2 | |
left 6 | |
fire | |
right 5 | |
fire | |
end |
При этом вы должны получить выходные данные координат и fired_at’s робота.
Вот так легко разработать DSL! Напомним, что это все, что мы сделали:
- Сделал класс
- Добавлены методы (необязательно, хотя без них DSL на самом деле ничего не значит)
- Добавлен конструктор (необязательно)
- Используется instance_eval
Это все, что нужно сделать! Неудивительно, что DSL настолько популярны — они дают нам много энергии для очень небольшого кода.
Помните, что у вас все еще есть Руби. Рассмотрим этот фрагмент кода:
class Robot | |
def initialize | |
@coordinates = [0, 0] | |
@fired_at = [] | |
end | |
def forward(n) | |
@coordinates[0] += n | |
puts @coordinates, «——« | |
end | |
def backward(n) | |
@coordinates[0] -= n | |
puts @coordinates, «——« | |
end | |
def right(n) | |
@coordinates[1] += n | |
puts @coordinates, «——« | |
end | |
def left(n) | |
@coordinates[1] -= n | |
puts @coordinates, «——« | |
end | |
def fire | |
@fired_at.push(@coordinates) | |
puts @coordinates | |
puts @fired_at, «——« | |
end | |
end | |
Robot.new.instance_eval do | |
def do_nothing | |
forward 1 | |
backward 1 | |
end | |
do_nothing | |
right 6 | |
end |
Если вы запустите его, то увидите, что робот заканчивается (0, 6). Мы использовали Ruby для создания некоторой абстракции для робота, чтобы мы могли создавать большие программы!
подсказки
При разработке DSL нужно помнить несколько вещей:
- Держите очень простой API — короткие имена, простые идеи
- Помните, что Ruby на вашей стороне — используйте его в полной мере вместе с вашим DSL
- DSL расшифровывается как Domain Specific Language — вы не пытаетесь изобрести здесь другой Ruby, так что держите его маленьким
- Если DSL будет введен пользователем на сервер — никогда не доверяйте своим пользователям
Последний пункт заслуживает более подробного объяснения. Если вы используете Robot DSL для сопоставления пользовательских роботов друг с другом, в условиях, когда кто-то загружает свой код на ваш сервер, а затем ваш сервер запускает код, будьте предельно осторожны в отношении того, что могут сделать ваши пользователи!
DSL НЕ защищают вас от того, что может сделать Ruby — весь код должен быть изолирован, и (в основном) весь доступ к syscall должен быть отключен. Если внешний код будет запущен на ваших серверах, вы будете взломаны, прежде чем сможете сказать «язык, специфичный для домена»!
Вывод
Я надеюсь, что вы получили удовольствие от поездки; мы разработали язык, специфичный для предметной области, примерно в 50 строках кода Это довольно круто!
Небо действительно предел с DSL! Их так легко построить, а еще проще сделать их лучше! Просто добавьте или измените методы в классе, для которого вызывается instance_eval.
Спасибо за чтение, и я надеюсь, что знания пригодятся.