- Сообщений: 247
- Спасибо получено: 537
Отражение на примере DSL
11 года 7 мес. назад - 11 года 7 мес. назад #73852
от Iren_Rin
Iren_Rin создал тему: Отражение на примере DSL
Сегодня я начну разговор о очень обширной и интересной теме - о метапрограммировании (у этого термина есть синоним - отражение, давайте будем использовать его, как более краткий). Так как тема большая, я не могу описать все в одной статье. Их будет несколько, и все они будут направлены на решение одной проблемы - проблемы описания данных в мейкере. Начнем со скиллов, возможно добавим туда еще врагов. Во время разработки своей боевой системы (которая пока еще совсем не готова), я понял, насколько же мне неудобно создавать навыки через интерфейс мейкера. Одни и те же действия мне приходилось делать по несколько раз, иногда для каждого навыка (а это меня просто бесит). Так же я успел добавить кучу кастомных методов, которых нельзя поменять из интерфейса базы данных. Я бы применил известный подход описания этих данных в графе 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. Набираем в консоли
Сперва нам нужно узнать класс скиллов
Ясно, наш DSL будет работать с этим классом. Давайте взглянем на публичные методы скилла
На приватные
На protected
Так как в руби классы - тоже объекты, можно посмотреть на методы класса RPG::Skill
2.2 Работа с instance переменными
Стоит сказать, что в мейкере широко используется довольно удачный подход - поведение объектов одого класса управляется instance переменными, при этом метод initialize обычно не принимает каких либо аргументов, или принимает, но очень мало. В initialize устанавливаются значения переменных по умолчанию, в итоге получается дефолтный объект, который затем можно изменить при помощи сеттеров, меняя значения instance переменных. При этом гетеров для этих переменных обычно нет.
Может это и звучит запутано, но это означает, что нас должны больше интересовать instance переменные, нежели методы. Получить их список можно так:
Получить значение instance переменной можно так
Установить значение instance переменной можно так
Обратите внимание, что все имена instance переменных начинаются с @, это очень важно, этот символ нельзя опускать в методах instance_variable_get и instance_variable_set
Маленькая ремарка о том что мы только что сделали.
Конечно же instance переменную внутри объекта проще (да и нужно, когда это возможно) вызывать просто по имени. Методы instance_variable_get и instance_variable_set используются обычно когда
а) Не известно точно имени переменной и \ или оно задается динамически.
б) Когда нужно работать с instance переменными снаружи объекта, а гетеров \ сетеров нет.
в) Во время исследования конечно же.
Так же к instance переменным можно достучаться к примеру при помощи eval и instance_eval
но это почти тоже самое, что и “забивать гвозди микроскопом”
Немного расскажу о динамической работе с константами, мы, скорее всего, не будем это использовать, но знать крайне полезно.
Выводы:
1 Объекты могут рассказать о себе все что угодно, какие у них есть методы, какие у них есть переменные, какие константы и т.п. Именно поэтому я скептически отношусь к попыткам скрыть руби скрипты, закодировать их код и т.п.
2 Теперь мы можем сформулировать, что же мы будем делать дальше - мы будем инициализировать объекты RPG::Skill, а потом устанавливать его instance variables (коих я насчитал аж 23 штуки) в нужные значения.
Думаю пока что это все, увидимся в следующей статье
Эта статья будет вводной, дальше я буду привязывать статьи к этому оглавлению (оно будет расти).
1 Введение
1.1 Что же такое DSL
1.2 Что нужно знать и что понадобится перед началом работы
2 Исследование
2.1 RPG::Skill
2.2 Работа с instance переменными
3. SkillDsl и SkillBuilder
1.1 Что же такое DSL
Для начала разберемся, что же мы будем писать - что же такое DSL? Вот пример того, что у нас в итоге получится (у меня уже есть на руках рабочий код (который впрочем я меняю чуть ли не каждый день), иначе я бы наверное и не взялся бы за статьи
ВНИМАНИЕ: Спойлер!
Code:
SkillDsl.define 'archer' do
skill 'Shot' do
tp_gain 5
id 147
shared 'shot' do
message 'shoots'
hit_type physical
shared 'range'
scope enemy
required_weapon bow
required_weapon gun
damage do
critical true
type to_hp
element common
formula 'a.atk * 2 + a.agi * 2 - b.def * 2'
end
end
end
skill 'Armor-piercing Shot' do
id 149
tp_cost 10
shared 'shot'
damage do
formula 'a.atk * 2 + a.agi * 2'
end
end
end
Это - 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, нам будет так проще. Нам нужно подгрузить скрипты дебаггера, для этого используем загрузчик (и заодно еще раз скажем спасибо Эльфу). Добавьте этот скрипт в скрипты проекта.
ВНИМАНИЕ: Спойлер!
Code:
class RequireLoader
module ToInclude
def require(path)
super
rescue Exception => e
RequireLoader.new(path).load
rescue
raise e
end
end
class << self
attr_accessor :binding
def pathes
@pathes ||= []
end
def enabled?
Dir.pwd.encode 'utf-8'
false
rescue Encoding::UndefinedConversionError
true
end
end
def initialize(path)
@path = path.sub(/\.rb\z/, '')
end
def load
File.open founded_path do |file|
eval file.lines.to_a.join, self.class.binding
end
end
def founded_path
all_pathes.find { |file_name| File.exist? file_name } || raise(LoadError)
end
def all_pathes
["#{@path}.rb"] + self.class.pathes.map do |path|
File.join path, "#{@path}.rb"
end
end
end
if RequireLoader.enabled?
include RequireLoader::ToInclude
RequireLoader.binding = binding
end
class SideScriptsLoader
class << self
def load(dir)
new(dir).load
end
def add_to_path(dir)
new(dir).add_to_path
end
def add_gems_to_path
if Dir.exist? 'gems'
Dir.entries('gems').each do |entry|
if Dir.exist? File.join('gems', entry)
new(File.join 'gems', entry, 'lib').add_to_path
end
end
end
end
end
def initialize(dir)
@dir = dir
end
def dirname
RequireLoader.enabled? ? @dir : File.expand_path(@dir, Dir.pwd)
end
def load
load_entries if Dir.exist? dirname
end
def add_to_path
if RequireLoader.enabled?
RequireLoader.pathes
else
$LOAD_PATH
end << dirname if Dir.exist? dirname
end
private
def load_entries
Dir.entries(dirname).each do |entry|
require filname(entry) if entry =~ /\.rb\Z/
end
end
def filname(entry)
if RequireLoader.enabled?
entry
else
File.expand_path File.join(dirname, entry), Dir.pwd
end
end
end
SideScriptsLoader.add_to_path 'lib/debugger'
SideScriptsLoader.load 'lib/debugger'
Запускаем наш проект (вместе с консолью), нажимаем прямо на фоне меню F5. Если все ок - активируется наша консоль.
Мейкер хранит все скиллы в массиве $data_skills. Набираем в консоли
Code:
> $data_skills #Первый элемент этого массива пустой, остальные - объекты скиллов
Code:
> $data_skills[1].class
=> RPG::Skill
Code:
> $data_skills[1].public_methods
Code:
> $data_skills[1].private_methods
Code:
> $data_skills[1].protected_methods
Code:
> $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 переменные, нежели методы. Получить их список можно так:
Code:
> $data_skills[1].instance_variables # Вернет массив имен переменных
Code:
> $data_skills[1].instance_variable_get :@description # Принимает строку или символ с именем переменной
Code:
> $data_skills[1].instance_variable_set :@description, ‘New’
ВНИМАНИЕ: Спойлер!
Маленькая ремарка о том что мы только что сделали.
Конечно же instance переменную внутри объекта проще (да и нужно, когда это возможно) вызывать просто по имени. Методы instance_variable_get и instance_variable_set используются обычно когда
а) Не известно точно имени переменной и \ или оно задается динамически.
б) Когда нужно работать с instance переменными снаружи объекта, а гетеров \ сетеров нет.
в) Во время исследования конечно же.
Так же к instance переменным можно достучаться к примеру при помощи eval и instance_eval
Code:
object.eval “@a = 1”
object.instance_eval { @a }
Немного расскажу о динамической работе с константами, мы, скорее всего, не будем это использовать, но знать крайне полезно.
ВНИМАНИЕ: Спойлер!
Code:
class Outer
ARRAY = [1, 2]
class Inner
end
end
Outer.constants #список констант
=> [:ARRAY, :Inner]
Outer.const_get :ARRAY
=> [1, 2]
Outer.const_get :Inner
Outer::Inner
Outer.const_set :HELLO, 'hello'
=> "hello"
Outer.const_get :HELLO
=> "hello
Выводы:
1 Объекты могут рассказать о себе все что угодно, какие у них есть методы, какие у них есть переменные, какие константы и т.п. Именно поэтому я скептически отношусь к попыткам скрыть руби скрипты, закодировать их код и т.п.
2 Теперь мы можем сформулировать, что же мы будем делать дальше - мы будем инициализировать объекты RPG::Skill, а потом устанавливать его instance variables (коих я насчитал аж 23 штуки) в нужные значения.
Думаю пока что это все, увидимся в следующей статье
Последнее редактирование: 11 года 7 мес. назад пользователем Iren_Rin.
Спасибо сказали: Lekste, DeadElf79, Ren310, strelokhalfer, caveman, Amphilohiy, Jas6666, yuryol, MaltonTheWarrior
Пожалуйста Войти или Регистрация, чтобы присоединиться к беседе.
11 года 7 мес. назад #73854
от DeadElf79
DeadElf79 ответил в теме Отражение на примере DSL
Отлично) Ждем продолжения))
Пожалуйста Войти или Регистрация, чтобы присоединиться к беседе.
11 года 7 мес. назад #73857
от Amphilohiy
Я верю, что иногда компьютер сбоит, и он выдает неожиданные результаты, но остальные 100% случаев это чья-то криворукость.
Amphilohiy ответил в теме Отражение на примере DSL
Можно дебаггером, а можно и в справке RPG:: классы найти, их там не шибко то и скрывают.
Но тоже жду продолжения!
Но тоже жду продолжения!
Я верю, что иногда компьютер сбоит, и он выдает неожиданные результаты, но остальные 100% случаев это чья-то криворукость.
Пожалуйста Войти или Регистрация, чтобы присоединиться к беседе.
11 года 7 мес. назад #74110
от Iren_Rin
Iren_Rin ответил в теме Отражение на примере DSL
Продолжим наше погружение в отражение.
Создадим папку в проекте, назовем ее dsls - там будут хранится наши DSL. В папке dsls создадим еще один каталог - skills. Там будут хранится наши DSL конкретно по скиллам. В папке dsls/skills создадим файл archer.rb, в нем сохраним следующий код, который, по нашему мнению, должен создать навык, пока дефолтный.
Мы конечно хотим подгружать и исполнять файлы из dsls/skills. Добавим следующую строчку в наш загрузчик, после строк, где мы загрузили дебаггер (вы же не забыли про наш загрузчик в скриптах проекта, да?)
Настало время реализовать немного кода, чтобы наш DSL заработал. Код мы будем хранить в папке lib, создадим ее. В ней создадим файл skill_dsl.rb, подгрузим все файлы из папки lib, добавив это в загрузчик (кстати у нас в скриптах проекта только и будет этот загрузчик, все остальное мы рассуем по папочкам да файликам)
В сам же файл lib/skill_dsl.rb поместим следующий код.
И так, все методы, что мы определили внутри class << self - классовые. Метод define принимает блок и исполняет его в контексте объекта класса. Метод skill принимает блок, создает инстанс RPG::Skill, исполняет принятый блок в контексте этого инстанса, результат добовляет в массив skills. В общем, весь этот код опирается на две вещи - во первых на блоки, которые можно принять в агрумент, и передать другим методам; во вторых на метод instance_eval, который исполняет блок в контексте объекта
Теперь мы можем запустить консоль и набрать
Очень хорошо… и пока бесполезно, хотелось бы, чтобы наш скилл хоть имя что ли имел. Мы уже можем это сделать так:
Это будет работать, но выглядит некрасиво. Мы хотим использовать setter #name=.
Чтобы руби нас правильно понял, и действительно вызвал метод, а не создал переменную name мы вынуждены использовать self. В общем по-хорошему я бы хотел видеть это:
Если мы исполним этот код, то на данном этапе руби выдаст ошибку. Мы подошли к необходимости выделить отдельный класс для создания скиллов - SkillBuilder.
В папке lib создадим папку skill_dsl. В ней создадим файл skill_builder.rb, пока пустой. В файле lib/skill_dsl.rb в самом низу добавим строчку
Теперь мы хотим чтобы наш SkillBuilder управлял созданием скилла, а внутри SkillDsl.skill мы просто будем записывать в .skills результат. Перепишем SkillDsl.skill
Метод SkillBuilder#item вернет нам нужный instance RPG::Skill. Для такого абстрактного названия были свои причины, о которых потом.
Давайте теперь напишем необходимый код для SkillBuilder (в lib/skill_dsl/skill_builder.rb)
Я бы хотел, чтобы, когда мы вызываем к примеру метод name у нашего билдера, он вызывал соответствующий сеттер у @item
Настало время представить вам представить два новых инструмента:
1 самую мощную, и самую опасную вещь в отражении - method_missing. method_missing - метод, который вызывается у объекта тогда, когда руби не может найти в нем вызванный метод. По умолчанию он выбрасывает ошибку - NameError. В method_missing передается первым аргументом имя несуществующего метода, дальше идут аргументы, которые были переданы несуществующему методу и блок.
2 public_send - при помощи его можно динамически вызвать интересующий нас публичный метод. Он принимает первым аргументом строку или символ - имя метода, который мы будем вызывать. Дальше идут все аргументы, который нужно передать и блок.
И вот как мы это используем - добавим этот код внутрь класса SkillBuilder
И вот теперь мы уже можем писать так
Да, damage - это вам не скалярный объект, это объект класса RPG::UsableItem::Damage. И как его мы будем создавать - узнаем из следующей статьи. А пока что на этом остановимся - заходим в консоль и вызываем
На этом все, надеюсь было интересно.
Создадим папку в проекте, назовем ее dsls - там будут хранится наши DSL. В папке dsls создадим еще один каталог - skills. Там будут хранится наши DSL конкретно по скиллам. В папке dsls/skills создадим файл archer.rb, в нем сохраним следующий код, который, по нашему мнению, должен создать навык, пока дефолтный.
Code:
SkillDsl.define do
skill do
end
end
Code:
SideScriptsLoader.load 'dsls/skills'
Настало время реализовать немного кода, чтобы наш DSL заработал. Код мы будем хранить в папке lib, создадим ее. В ней создадим файл skill_dsl.rb, подгрузим все файлы из папки lib, добавив это в загрузчик (кстати у нас в скриптах проекта только и будет этот загрузчик, все остальное мы рассуем по папочкам да файликам)
Code:
#Будем вызовы .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'
Code:
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
Code:
obj = Object.new
obj.instance_eval do
@a = 1
end
obj.instance_variable_get :@a
# => 1
Теперь мы можем запустить консоль и набрать
Code:
> SkillDsl.skills
#=> Массив с одним инстансом RPG::Skill, который мы только что создали.
Code:
SkillDsl.define do
skill do
self.name = 'My New Name'
end
end
Это будет работать, но выглядит некрасиво. Мы хотим использовать setter #name=.
Чтобы руби нас правильно понял, и действительно вызвал метод, а не создал переменную name мы вынуждены использовать self. В общем по-хорошему я бы хотел видеть это:
Code:
SkillDsl.define do
skill do
name 'My New Name'
end
end
В папке lib создадим папку skill_dsl. В ней создадим файл skill_builder.rb, пока пустой. В файле lib/skill_dsl.rb в самом низу добавим строчку
Code:
require 'skill_dsl/skill_builder' #Это подгрузит skill_builder.rb
Теперь мы хотим чтобы наш SkillBuilder управлял созданием скилла, а внутри SkillDsl.skill мы просто будем записывать в .skills результат. Перепишем SkillDsl.skill
Code:
def skill(&block)
skills << SkillDsl::SkillBuilder.new.tap { |builder| builder.instance_eval(&block) }.item
end
Давайте теперь напишем необходимый код для SkillBuilder (в lib/skill_dsl/skill_builder.rb)
Code:
class SkillBuilder
attr_reader :item
def initialize
@item = RPG::Skill.new
end
end
Я бы хотел, чтобы, когда мы вызываем к примеру метод name у нашего билдера, он вызывал соответствующий сеттер у @item
Code:
#мы вызываем так
builder.name 'My New Name'
#хотим чтобы было так
builder.item.name = 'My New Name'
1 самую мощную, и самую опасную вещь в отражении - method_missing. method_missing - метод, который вызывается у объекта тогда, когда руби не может найти в нем вызванный метод. По умолчанию он выбрасывает ошибку - NameError. В method_missing передается первым аргументом имя несуществующего метода, дальше идут аргументы, которые были переданы несуществующему методу и блок.
2 public_send - при помощи его можно динамически вызвать интересующий нас публичный метод. Он принимает первым аргументом строку или символ - имя метода, который мы будем вызывать. Дальше идут все аргументы, который нужно передать и блок.
И вот как мы это используем - добавим этот код внутрь класса SkillBuilder
Code:
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
Code:
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
Code:
SkillDsl.skills #массив с одним единственным скиллом, у которого будут остановлены tp_cost, mp_cost и tp_gain
Пожалуйста Войти или Регистрация, чтобы присоединиться к беседе.
11 года 7 мес. назад #74124
от Amphilohiy
Я верю, что иногда компьютер сбоит, и он выдает неожиданные результаты, но остальные 100% случаев это чья-то криворукость.
Amphilohiy ответил в теме Отражение на примере DSL
На .tap косячок пришлось угробить, но в остальном понятно. Вообще method_missing порадовал, веселая штуковина.
Я верю, что иногда компьютер сбоит, и он выдает неожиданные результаты, но остальные 100% случаев это чья-то криворукость.
Пожалуйста Войти или Регистрация, чтобы присоединиться к беседе.
Время создания страницы: 0.097 секунд
