Программирование на языке Ruby.

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.

Добавлен метод meth1 в класс MyClass.

Добавлен метод meth2 в класс MyClass.

Отметим, что фактически добавлено три метода. Возможно, это противоречит вашим ожиданиям, но метод singleton_method_added может отследить и добавление самого себя.

Метод inherited (из Class) используется примерно так же. Он вызывается в момент создания подкласса.

Class MyClass.

 def MyClass.inherited(subclass).

  puts "#{subclass} наследует MyClass.".

 end.

 # ...

End.

Class OtherClass < MyClass.

 # ...

End.

# Выводится: OtherClass наследует MyClass.

Можно также следить за добавлением методов экземпляра модуля к объекту (с помощью метода extend). При каждом выполнении extend вызывается метод extend_object.

Module MyMod.

 def MyMod.extend_object(obj).

  puts "Расширяется объект id #{obj.object_id}, класс #{obj.class}".

  super.

 end.

 # ...

End.

X = [1, 2, 3].

X.extend(MyMod).

# Выводится:

# Расширяется объект id 36491192, класс Array.

Обращение к super необходимо для того, чтобы мог отработать исходный метод extend_object. Это напоминает поведение метода append_features (см. раздел 11.1.12); данный метод годится также для отслеживания использования модулей.