Войти на сайт

Авторизация, ждите ...
×

ТЕМА: Отражение на примере DSL

Отражение на примере DSL 9 года 7 мес. назад #73852

  • Iren_Rin
  • Iren_Rin аватар
  • Вне сайта
  • Мастер
  • Сообщений: 247
  • Спасибо получено: 537
  • КоммерсантПроект месяца 1 местоПрограммист RubyПроект года 1 местоУчитель
Сегодня я начну разговор о очень обширной и интересной теме - о метапрограммировании (у этого термина есть синоним - отражение, давайте будем использовать его, как более краткий). Так как тема большая, я не могу описать все в одной статье. Их будет несколько, и все они будут направлены на решение одной проблемы - проблемы описания данных в мейкере. Начнем со скиллов, возможно добавим туда еще врагов. Во время разработки своей боевой системы (которая пока еще совсем не готова), я понял, насколько же мне неудобно создавать навыки через интерфейс мейкера. Одни и те же действия мне приходилось делать по несколько раз, иногда для каждого навыка (а это меня просто бесит). Так же я успел добавить кучу кастомных методов, которых нельзя поменять из интерфейса базы данных. Я бы применил известный подход описания этих данных в графе notes, но в мейкерском руби нет YAML, нет JSON, нет XML, в общем пришлось бы самому писать парсер. А еще я бы хотел чтобы эти скиллы можно было легко передавать между проектами (я понимаю, что это можно сделать несколькими путями). В итоге я написал свой DSL для навыков, и на этом примере очень удобно будет рассмотреть многие аспекты отражения.
Эта статья будет вводной, дальше я буду привязывать статьи к этому оглавлению (оно будет расти).

1 Введение
1.1 Что же такое DSL
1.2 Что нужно знать и что понадобится перед началом работы

2 Исследование
2.1 RPG::Skill
2.2 Работа с instance переменными

3. SkillDsl и SkillBuilder

1.1 Что же такое DSL
Для начала разберемся, что же мы будем писать - что же такое DSL? Вот пример того, что у нас в итоге получится (у меня уже есть на руках рабочий код (который впрочем я меняю чуть ли не каждый день), иначе я бы наверное и не взялся бы за статьи :))
ВНИМАНИЕ: Спойлер! [ Нажмите, чтобы развернуть ]

Это - DSL (Domain Specific Language) , и для начала нужно понять, что это валидный руби код. Никакой магии тут нет. Тут все те же методы, блоки, классы и т.п. DSL предназначен для упрощения написания кода в какой-то специфической области (именно поэтому и Domain Specific). В нашем случае - для упрощения описания скилов. C хорошо написанным и документированным DSL может работать человек, далекий от руби. В принципе в этом может быть одна из целей написания DSL.

1.2 Что нужно знать и что понадобится перед началом работы
a) Нам понадобится дебаггер, теоретически можно и без него, но только теоретически.
б) Прочитайте статью о массивах
в) Так же обязательно прочитайте статью о блоках, их будет много, они будут разными :)
г) Обязательно понимание ООП (про это я пока не писал, но уже планирую). Я не буду отвлекаться на объяснения таких понятий как “объект”, “класс” или “instance переменная”. Иначе это растянется на год.

2 Исследование
Для начала нужно разобраться в чем проблема - мы хотим создавать при помощи нашей DSL объекты навыков. Начнем попорядку, для начала посмотрим что же из себя представляют эти самые навыки.

2.1 RPG::Skill
Сделаем новый проект, в нем сделаем папку lib, в ней будет жить наш код. Скачаем с гитхаба код дебаггера и распакуем его в эту папку. Переименуем папку rpg_maker_debugger-master в просто debugger, нам будет так проще. Нам нужно подгрузить скрипты дебаггера, для этого используем загрузчик (и заодно еще раз скажем спасибо Эльфу). Добавьте этот скрипт в скрипты проекта.
ВНИМАНИЕ: Спойлер! [ Нажмите, чтобы развернуть ]


Запускаем наш проект (вместе с консолью), нажимаем прямо на фоне меню F5. Если все ок - активируется наша консоль.
Мейкер хранит все скиллы в массиве $data_skills. Набираем в консоли
> $data_skills #Первый элемент этого массива  пустой, остальные - объекты скиллов
Сперва нам нужно узнать класс скиллов
> $data_skills[1].class
=> RPG::Skill
Ясно, наш DSL будет работать с этим классом. Давайте взглянем на публичные методы скилла
> $data_skills[1].public_methods
На приватные
> $data_skills[1].private_methods
На protected
> $data_skills[1].protected_methods
Так как в руби классы - тоже объекты, можно посмотреть на методы класса RPG::Skill
> $data_skills[1].class.private_methods
> $data_skills[1].class.public_methods
#а вот это доступно только для классов
> $data_skills[1].class.instance_methods

2.2 Работа с instance переменными
Стоит сказать, что в мейкере широко используется довольно удачный подход - поведение объектов одого класса управляется instance переменными, при этом метод initialize обычно не принимает каких либо аргументов, или принимает, но очень мало. В initialize устанавливаются значения переменных по умолчанию, в итоге получается дефолтный объект, который затем можно изменить при помощи сеттеров, меняя значения instance переменных. При этом гетеров для этих переменных обычно нет.
Может это и звучит запутано, но это означает, что нас должны больше интересовать instance переменные, нежели методы. Получить их список можно так:
> $data_skills[1].instance_variables # Вернет массив имен переменных
Получить значение instance переменной можно так
> $data_skills[1].instance_variable_get :@description # Принимает строку или символ с именем переменной
Установить значение instance переменной можно так
> $data_skills[1].instance_variable_set :@description,New
Обратите внимание, что все имена instance переменных начинаются с @, это очень важно, этот символ нельзя опускать в методах instance_variable_get и instance_variable_set
ВНИМАНИЕ: Спойлер! [ Нажмите, чтобы развернуть ]

Немного расскажу о динамической работе с константами, мы, скорее всего, не будем это использовать, но знать крайне полезно.
ВНИМАНИЕ: Спойлер! [ Нажмите, чтобы развернуть ]


Выводы:
1 Объекты могут рассказать о себе все что угодно, какие у них есть методы, какие у них есть переменные, какие константы и т.п. Именно поэтому я скептически отношусь к попыткам скрыть руби скрипты, закодировать их код и т.п.
2 Теперь мы можем сформулировать, что же мы будем делать дальше - мы будем инициализировать объекты RPG::Skill, а потом устанавливать его instance variables (коих я насчитал аж 23 штуки) в нужные значения.

Думаю пока что это все, увидимся в следующей статье :)
Последнее редактирование: 9 года 7 мес. назад от Iren_Rin.
Администратор запретил публиковать записи гостям.
За этот пост поблагодарили: Lekste, DeadElf79, Ren310, strelokhalfer, caveman, Amphilohiy, Jas6666, yuryol, MaltonTheWarrior

Отражение на примере DSL 9 года 7 мес. назад #73854

  • DeadElf79
  • DeadElf79 аватар
  • Вне сайта
  • Звездный Страж
  • Сообщений: 3147
  • Спасибо получено: 2650
  • 3 местоВетеранПрограммист RubyПисатель 3 место1 место в ГотвУчительПроект месяца 2 местоПроект месяца 1 местоОрганизатор конкурсов
Отлично) Ждем продолжения))
Администратор запретил публиковать записи гостям.

Отражение на примере DSL 9 года 7 мес. назад #73857

  • Amphilohiy
  • Amphilohiy аватар
  • Вне сайта
  • Светлый дракон
  • Сообщений: 547
  • Спасибо получено: 666
  • 2 место ГотвОраторПобедитель Сбитой кодировкиПрограммист RubyУчитель
Можно дебаггером, а можно и в справке RPG:: классы найти, их там не шибко то и скрывают.
Но тоже жду продолжения!
Я верю, что иногда компьютер сбоит, и он выдает неожиданные результаты, но остальные 100% случаев это чья-то криворукость.
Администратор запретил публиковать записи гостям.

Отражение на примере DSL 9 года 7 мес. назад #74110

  • Iren_Rin
  • Iren_Rin аватар
  • Вне сайта
  • Мастер
  • Сообщений: 247
  • Спасибо получено: 537
  • КоммерсантПроект месяца 1 местоПрограммист RubyПроект года 1 местоУчитель
Продолжим наше погружение в отражение.
Создадим папку в проекте, назовем ее dsls - там будут хранится наши DSL. В папке dsls создадим еще один каталог - skills. Там будут хранится наши DSL конкретно по скиллам. В папке dsls/skills создадим файл archer.rb, в нем сохраним следующий код, который, по нашему мнению, должен создать навык, пока дефолтный.
SkillDsl.define do
  skill do
  end
end
Мы конечно хотим подгружать и исполнять файлы из dsls/skills. Добавим следующую строчку в наш загрузчик, после строк, где мы загрузили дебаггер (вы же не забыли про наш загрузчик в скриптах проекта, да?)
SideScriptsLoader.load 'dsls/skills'

Настало время реализовать немного кода, чтобы наш DSL заработал. Код мы будем хранить в папке lib, создадим ее. В ней создадим файл skill_dsl.rb, подгрузим все файлы из папки lib, добавив это в загрузчик (кстати у нас в скриптах проекта только и будет этот загрузчик, все остальное мы рассуем по папочкам да файликам)
#Будем вызовы .add_to_path держать выше всех вызовов .load 
#Так же SideScriptsLoader.load ‘lib’ Должен идти выше, чем SideScriptsLoader.load ‘dsls/skills’
#В общем у вас должно быть так (после кода собственно самого загрузчика)
SideScriptsLoader.add_to_path 'lib/debugger'
SideScriptsLoader.add_to_path 'lib'
SideScriptsLoader.add_to_path 'lib/skill_dsl'
SideScriptsLoader.load 'lib/debugger'
SideScriptsLoader.load 'lib'
SideScriptsLoader.load 'dsls/skills'
 
В сам же файл lib/skill_dsl.rb поместим следующий код.
class SkillDsl
  class << self
    def define(&block)
      instance_eval(&block)
    end
 
    def skill(&block)
      skills << RPG::Skill.new.tap { |skill| skill.instance_eval(&block) }
    end
 
    def skills
      @skills ||= []
    end
  end
end
И так, все методы, что мы определили внутри class << self - классовые. Метод define принимает блок и исполняет его в контексте объекта класса. Метод skill принимает блок, создает инстанс RPG::Skill, исполняет принятый блок в контексте этого инстанса, результат добовляет в массив skills. В общем, весь этот код опирается на две вещи - во первых на блоки, которые можно принять в агрумент, и передать другим методам; во вторых на метод instance_eval, который исполняет блок в контексте объекта
obj = Object.new
obj.instance_eval do
  @a = 1
end
obj.instance_variable_get :@a
# => 1

Теперь мы можем запустить консоль и набрать
> SkillDsl.skills
#=> Массив с одним инстансом RPG::Skill, который мы только что создали.
Очень хорошо… и пока бесполезно, хотелось бы, чтобы наш скилл хоть имя что ли имел. Мы уже можем это сделать так:
SkillDsl.define do
  skill do
    self.name = 'My New Name'
  end
end

Это будет работать, но выглядит некрасиво. Мы хотим использовать setter #name=.
Чтобы руби нас правильно понял, и действительно вызвал метод, а не создал переменную name мы вынуждены использовать self. В общем по-хорошему я бы хотел видеть это:
SkillDsl.define do
  skill do
    name 'My New Name'
  end
end
Если мы исполним этот код, то на данном этапе руби выдаст ошибку. Мы подошли к необходимости выделить отдельный класс для создания скиллов - SkillBuilder.
В папке lib создадим папку skill_dsl. В ней создадим файл skill_builder.rb, пока пустой. В файле lib/skill_dsl.rb в самом низу добавим строчку
require 'skill_dsl/skill_builder' #Это подгрузит skill_builder.rb

Теперь мы хотим чтобы наш SkillBuilder управлял созданием скилла, а внутри SkillDsl.skill мы просто будем записывать в .skills результат. Перепишем SkillDsl.skill
def skill(&block)
  skills << SkillDsl::SkillBuilder.new.tap { |builder| builder.instance_eval(&block) }.item
end
Метод SkillBuilder#item вернет нам нужный instance RPG::Skill. Для такого абстрактного названия были свои причины, о которых потом.

Давайте теперь напишем необходимый код для SkillBuilder (в lib/skill_dsl/skill_builder.rb)
class SkillBuilder
  attr_reader :item
 
  def initialize
    @item = RPG::Skill.new
  end
end

Я бы хотел, чтобы, когда мы вызываем к примеру метод name у нашего билдера, он вызывал соответствующий сеттер у @item
#мы вызываем так
builder.name 'My New Name'
#хотим чтобы было так
builder.item.name = 'My New Name'
Настало время представить вам представить два новых инструмента:
1 самую мощную, и самую опасную вещь в отражении - method_missing. method_missing - метод, который вызывается у объекта тогда, когда руби не может найти в нем вызванный метод. По умолчанию он выбрасывает ошибку - NameError. В method_missing передается первым аргументом имя несуществующего метода, дальше идут аргументы, которые были переданы несуществующему методу и блок.
2 public_send - при помощи его можно динамически вызвать интересующий нас публичный метод. Он принимает первым аргументом строку или символ - имя метода, который мы будем вызывать. Дальше идут все аргументы, который нужно передать и блок.
И вот как мы это используем - добавим этот код внутрь класса SkillBuilder
  def method_missing(*args, &block) #Эта не простая звездочка, она упакует все аргументы в массив
    method_name = :"#{args[0]}="   #Тут мы генерим имя вызываемого метода в виде сивола
    if @item.public_methods.include? method_name #вот зачем нужен символ, а не строка - в public_methods находятся именно символы.
                                                 # Мы собираемся вызывать сеттер только если он на самом деле есть у @item
      @item.public_send(method_name, *args[1 .. -1], &block) #тут мы собственно и вызываем публичный сеттер, передаем ему все возможные аргументы и блок. Звездочка и тут нам помогает - она распоковывает массив на аргументы
    else
      super #если сеттера нет - мы просто хотим, чтобы был вызван method_missing у родителя, который выкенет нам эксепшн
    end
  end
 
И вот теперь мы уже можем писать так
SkillDsl.define do
  skill do
    name 'powershot'
     #что там еще нам нужно было для навыка? (вспоминаем что мы увидели в instance_variables у скиллов из $data_skills)
    #установить сколько tp будет боец получать за использование? не вопрос
    tp_gain 5 
    #стоимость навыка в tp - легко
    tp_cost 5 
    #стоимость в mp - туда же
    mp_cost 10 
    #damage хм… какой такой damage? это вообще что такое?
  end
end
Да, damage - это вам не скалярный объект, это объект класса RPG::UsableItem::Damage. И как его мы будем создавать - узнаем из следующей статьи. А пока что на этом остановимся - заходим в консоль и вызываем
SkillDsl.skills #массив с одним единственным скиллом, у которого будут остановлены tp_cost, mp_cost и tp_gain
На этом все, надеюсь было интересно.
Администратор запретил публиковать записи гостям.
За этот пост поблагодарили: DeadElf79, Ren310, strelokhalfer, Amphilohiy

Отражение на примере DSL 9 года 6 мес. назад #74124

  • Amphilohiy
  • Amphilohiy аватар
  • Вне сайта
  • Светлый дракон
  • Сообщений: 547
  • Спасибо получено: 666
  • 2 место ГотвОраторПобедитель Сбитой кодировкиПрограммист RubyУчитель
На .tap косячок пришлось угробить, но в остальном понятно. Вообще method_missing порадовал, веселая штуковина.
Я верю, что иногда компьютер сбоит, и он выдает неожиданные результаты, но остальные 100% случаев это чья-то криворукость.
Администратор запретил публиковать записи гостям.
Время создания страницы: 1.197 секунд