SCoon
Monkey See, Monkey Patch
27.12.2012 в 13:42 (6524 Просмотров)
Читая чужие скрипты, постоянно встречаю удивляющий меня фрагмент. На примере из все того же "Scripting in RGSS Ruby for Intermediate and Experts":
(Это "исходный" вариант -- выполненная Blizzard'ом переделка не заслуживает упоминания ввиду полной убогости).Код:class Scene_Map alias_method :s____partycycling_scnmap_update, :update def update if Input.trigger?(Input::L) $game_party.shift_forward elsif Input.trigger?(Input::R) $game_party.shift_backwards end s____partycycling_scnmap_update end end
Что мы здесь видим? Каждый раз автор такого фрагмента заново вымучивает уникальный префикс, создает алиас метода и переопределяет его. Каждый раз эта пляска с бубном мешает понять, ради чего все это затеяно. Помня о том, что мы имеет дело с Ruby, который очень доброжелателен к программистам, попробуем записать то же более прозрачно.
В чем суть этого кода? Очень просто: выполнить ДО метода update некоторый нужный нам код. Вот так было бы понятнее, не правда ли?
Читается очевидно: "до update сделать это". В таком виде понять смысл действий проще, не правда ли? Другой характерный пример -- дополнительная инициализация:Код:class Scene_Map before :update do if Input.trigger?(Input::L) $game_party.shift_forward elsif Input.trigger?(Input::R) $game_party.shift_backwards end end end
Поскольку подобный код нужен часто, а слово initialize лично у меня -- источник постоянных опечаток, хорошо бы иметь возможность писать еще короче и понятнее:Код:class Game_System after :initialize do @gold_log = [] end end
Кода меньше, код понятнее. Как следствие -- пишем быстрее, читаем быстрее, делаем меньше ошибок.Код:class Game_System after_init do @gold_log = [] end end
Одна беда -- штатно Ruby этого не умеет. Но мы ему поможем. Самый простой способ -- дописать реализацию before, after и т.п. непосредственно в Class. При этом мы не будем изобретать велосипед и внутри, "под ковром", будем использовать все тот же monkey patching. Прежде всего, нам потребуется метод для формирования уникальных алиасов:
Здесь все просто: алиас формируется добавлением префикса к имени метода. Поскольку мы имеем право захотеть проалиасить метод несколько раз -- добавляем еще и инкрементируемый счетчик. Итак, на входе символ с именем метода, на выходе -- символ с созданным уникальным алиасом. Теперь мы можем написать что-нибудь полезное:Код:class Class private @@alias_index = 0 def create_unique_alias(method) @@alias_index += 1 original = "__alias#{@@alias_index}_#{method}".to_sym class_eval %Q{ alias_method :#{original}, :#{method} } original end end
Код, в основом, простой: имея свежесозданный алиас, переопределяем метод, а внутри него вызываем (через __send__) старый метод по алиасу. Затем вызываем блок с кодом, который нам передали (он получит параметры, с которыми был вызван метод). Важный нюанс -- вызов через instance_exec. Дело в том, что параметр block содержит блок кода, определенный в контексте класса, а не экземпляра. Если мы просто напишем "block.call(*args)", то не сможем достучаться до переменных и методов объекта.Код:def after(method, &block) original = create_unique_alias(method) define_method method do |*args| self.__send__(original, *args) instance_exec(*args, &block) end end
На основе after реализуем специальный случай -- добавление кода к конструктору:
Реализация before практически идентична реализации after:Код:def after_init(&block) after :initialize, &block end
Разница лишь в порядке вызовов. В большинстве случаев before и after будет достаточно. Однако, изредка требуется вызвать старый метод из середины своего кода, да еще несколько раз или с измененными параметрами. Для таких случаев мы реализуем instead_of, который дает нужную свободу выбора момента для вызова оригинального метода и позволяет подменять его параметры:Код:def before(method, &block) original = create_unique_alias(method) define_method method do |*args| instance_exec(*args, &block) self.__send__(original, *args) end end
Здесь есть интересный нюанс: чтобы наш блок кода мог вызвать исходный метод, мы завернем его в процедуру и передадим в блок первым параметром. Пример использования:Код:def instead_of(method, &block) original = create_unique_alias(method) define_method method do |*args| original_block = proc { |*args| self.__send__(original, *args) } actual_args = [ original_block ] + args instance_exec(*actual_args, &block) end end
Ruby -- красивый и удобный язык. Используйте его, чтобы сделать свою жизнь приятнее!Код:class Game_Party instead_of :gain_gold do |original, amount| adjusted_amount = [100, amount].min original.call(adjusted_amount) end end








Отправить другу ссылку на эту запись
