не хочет обрабатывать конкретный вызов, она должна вызвать
super
, а не возбуждать исключение. Тогда методы
method_missing
в суперклассах получат возможность разобраться с ситуацией. В конечном счете будет вызван
method_missing
, определенный в классе
Object
, который и возбудит исключение.
11.3.13. Отслеживание изменений в определении класса или объекта
А зачем, собственно? Кому интересны изменения, которым
подвергался класс?
Одна возможная причина — желание следить за состоянием выполняемой программы на Ruby. Быть может, мы реализуем графический отладчик, который должен обновлять список методов, добавляемых «на лету».
Другая причина: мы хотим вносить соответствующие изменения в другие классы. Например, мы разрабатываем модуль, который можно включить в определение любого класса. С момента включения будут трассироваться любые обращения к методам этого класса. Что-то в этом роде:
class MyClass
include Tracing
def one
end
def two(x, y)
end
end
m = MyClass.new
m.one # Вызван метод one. Параметры =
m.two(1, 'cat') # Вызван метод two. Параметры = 1, cat
Он должен работать также для всех подклассов трассируемого класса:
class Fred < MyClass
def meth(*a)
end
end
Fred.new.meth{2,3,4,5) # вызван метод meth. Параметры =2, 3, 4, 5
Возможная реализация такого модуля показана в листинге 11.18.
Листинг 11.18. Трассирующий модуль
module Tracing
def Tracing.included(into)
into.instance_methods(false).each { |m|
Tracing.hook_method(into, m) }
def into.method_added(meth)
unless @adding
@adding = true
Tracing.hook_method(self, meth)
@adding = false
end
end
end
def Tracing.hook_method(klass, meth)
klass.class_eval do
alias_method "old_#{meth}", "#{meth}"
define_method(meth) do |*args|
puts "Вызван метод #{meth}. Параметры = #{args.join(', ')}"
self.send("old_#{meth}",*args)
end
end
end
end
class MyClass
include Tracing
def first_meth
end
def second_meth(x, y)
end
end
m = MyClass.new
m.first_meth #
Вызван метод first_meth. Параметры =
m.second_meth(1, 'cat') # Вызван метод second_meth. Параметры = 1, cat
В этом коде два основных метода. Первый,
included
, вызывается при каждой вставке модуля в класс. Наша версия делает две вещи: вызывает метод
hook_method
каждого метода, уже определенного в целевом классе, и вставляет определение метода
method_added
в этот класс. В результате любой добавленный позже метод тоже будет обнаружен и для него вызван
hook_method
. Сам метод
hook_method
работает прямолинейно. При добавлении метода ему назначается синоним
old_name
. Исходный метод заменяется кодом трассировки, который выводит имя и параметры метода, а затем вызывает метод, к которому было обращение.
Обратите внимание на использование конструкции
alias_method
. Работает она почти так же, как
alias
, но только для методов (да и сама является методом, а не ключевым словом). Можно было бы записать эту строку иначе:
# Еще два способа записать эту строку...
# Символы с интерполяцией:
alias_method :"old_#{meth}", :"#{meth}"
# Преобразование строк с помощью to_sym:
alias_method "old_#{meth}".to_sym, meth.to_sym
Чтобы обнаружить добавление нового метода класса в класс или модуль, можно определить метод класса
singleton_method_added
внутри данного класса. (Напомним, что синглетный метод в этом смысле — то, что мы обычно называем методом класса, поскольку Class — это объект.) Этот метод определен в модуле
Kernel
и по умолчанию ничего не делает, но мы можем переопределить его, как сочтем нужным.
class MyClass
def MyClass.singleton_method_added(sym)
puts "Добавлен метод #{sym.to_s} в класс MyClass."
end
def MyClass.meth1 puts "Я meth1."
end
end
def MyClass.meth2
puts "А я meth2."
end
В результате выводится следующая информация:
Добавлен метод singleton_method_added в класс MyClass.