Читая чужие скрипты, постоянно встречаю удивляющий меня фрагмент. На примере из все того же "Scripting in RGSS Ruby for Intermediate and Experts":
Код:
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
(Это "исходный" вариант -- выполненная Blizzard'ом переделка не заслуживает упоминания ввиду полной убогости).
Что мы здесь видим? Каждый раз автор такого фрагмента заново вымучивает уникальный префикс, создает алиас метода и переопределяет его. Каждый раз эта пляска с бубном мешает понять, ради чего все это затеяно. Помня о том, что мы имеет дело с Ruby, который очень доброжелателен к программистам, попробуем записать то же более прозрачно.
В чем суть этого кода? Очень просто: выполнить ДО метода 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
Читается очевидно: "до update сделать это". В таком виде понять смысл действий проще, не правда ли? Другой характерный пример -- дополнительная инициализация:
Код:
class Game_System
after :initialize do
@gold_log = []
end
end
Поскольку подобный код нужен часто, а слово initialize лично у меня -- источник постоянных опечаток, хорошо бы иметь возможность писать еще короче и понятнее:
Код:
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
Здесь все просто: алиас формируется добавлением префикса к имени метода. Поскольку мы имеем право захотеть проалиасить метод несколько раз -- добавляем еще и инкрементируемый счетчик. Итак, на входе символ с именем метода, на выходе -- символ с созданным уникальным алиасом. Теперь мы можем написать что-нибудь полезное:
Код:
def after(method, &block)
original = create_unique_alias(method)
define_method method do |*args|
self.__send__(original, *args)
instance_exec(*args, &block)
end
end
Код, в основом, простой: имея свежесозданный алиас, переопределяем метод, а внутри него вызываем (через __send__) старый метод по алиасу. Затем вызываем блок с кодом, который нам передали (он получит параметры, с которыми был вызван метод). Важный нюанс -- вызов через instance_exec. Дело в том, что параметр block содержит блок кода, определенный в контексте класса, а не экземпляра. Если мы просто напишем "block.call(*args)", то не сможем достучаться до переменных и методов объекта.
На основе after реализуем специальный случай -- добавление кода к конструктору:
Код:
def after_init(&block)
after :initialize, &block
end
Реализация before практически идентична реализации after:
Код:
def before(method, &block)
original = create_unique_alias(method)
define_method method do |*args|
instance_exec(*args, &block)
self.__send__(original, *args)
end
end
Разница лишь в порядке вызовов. В большинстве случаев before и after будет достаточно. Однако, изредка требуется вызвать старый метод из середины своего кода, да еще несколько раз или с измененными параметрами. Для таких случаев мы реализуем instead_of, который дает нужную свободу выбора момента для вызова оригинального метода и позволяет подменять его параметры:
Код:
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
Здесь есть интересный нюанс: чтобы наш блок кода мог вызвать исходный метод, мы завернем его в процедуру и передадим в блок первым параметром. Пример использования:
Код:
class Game_Party
instead_of :gain_gold do |original, amount|
adjusted_amount = [100, amount].min
original.call(adjusted_amount)
end
end
Ruby -- красивый и удобный язык. Используйте его, чтобы сделать свою жизнь приятнее!