Порядок выполнения callback-ов при наследовании

в 8:54, , рубрики: callbacks, ruby, наследование, метки: ,

Ruby — очень интересный язык. Одной из его особенностей является возможность выполнения заданных функций при добавлении модуля в класс. Стандартный пример выглядит следующим образом:

module MyModule
  module InstanceMethods
  end

  module ClassMethods
  end

  def self.included(base)
    base.include(InstanceMethods)
    base.extend(ClassMethods)
  end
end

Здесь создаются два под-модуля в рамках текущего модуля для разделения методов инстанса и методов класса. При «примешивании» модуля MyModule в класс выполняется функция included, которая добавляет необходимые методы класса и методы объектов класса.

Не так давно я открыл для себя еще одну подобную функцию, которая выполняется при наследовании

class Ancestor
  def self.inherited(successor)
  end
end

class Successor < Ancestor
end

На мой взгляд, в некоторых ситуациях эта конструкция может быть очень удобна, к примеру в Ruby on Rails версии 3.0 и старше существует модуль InheritableAttributes, который позволяет копировать аттрибуты класса при наследовании. Вот простой пример использования данного модуля:

require "active_support/core_ext/class/inheritable_attributes"

class Base
  class_inheritable_accessor :color
end

Base.color = "red"

class Ancestor < Base
end

Ancestor.color # => "red"
Ancestor.color = "green"

Base.color # => "red"

Удобно? Вполне. Правда в последних версиях rails модуль inheritable_attributes был признан устаревшим и был заменен на class_attribute.

Как только вводится понятие callback-а, сразу же возникает вопрос о том, когда этот callback будет выполняться: до eval-а тела класса или после. Проверим:

class Base
  def self.inherited(m)
    puts "Hello from inherited callback"
  end
end

class NamedClass < Base
  puts "Hello from class body"
end

# Output:
# Hello from inherited callback
# Hello from class body

Т.е. callback выполняется до eval-а тела класса. И все бы хорошо, если не одно «но»: в ruby вместе с именованными классами существуют еще и анонимные, которые объявляются так:

anonymous_class = Class.new(Base) do
  # body
end

Портируя известный gem active_attr я столкнулся с такой особенностью: все spec-и при запуске в ruby ветки 1.9 проходят нормально, но стоит лишь попросить rvm использовать старый добрый ruby 1.8.7 (или ree) — так половина тестов начинает отваливаться без видимых на то причин (отмечу, что до моих коммитов, поддержки rails 3.0 не было и в связи с этим все spec-и отрабатывали корректно как на ruby ветки 1.9, так и на ruby ветки 1.8)

Как оказалось, причина в следующей особенности работы ruby 1.8.7:

class Base
  def self.inherited(m)
    puts "--> Hello from inherited callback"
  end 
end

puts "declare named class"
class NamedClass < Base
  puts "--> Hello from named class"
end

puts

puts "declare anonymous class"
Class.new(Base) do
  puts "--> Hello from anonymous class"
end

# Output for ruby 1.9.3
# declare named class
# --> Hello from inherited callback
# --> Hello from named class
#
# declare anonymous class
# --> Hello from inherited callback
# --> Hello from anonymous class


# Output for ruby 1.8.7
# declare named class
# --> Hello from inherited callback
# --> Hello from named class
#
# declare anonymous class
# --> Hello from anonymous class
# --> Hello from inherited callback

В ruby 1.8.7 для анонимного класса сначала eval-ится тело, а уже потом выполняется callback inherited. Таким образом, модуль InheritableAttributes, ожидающий пустого класса сталкивается с наличием некоторых методов и отрабатывает неправильно.

Как можно решить данную проблему?
1. перейти на ruby 1.9
2. не использовать функционал, опирающийся на callback inherited (если данный функционал не используется явно, а только в рамках rails — то достаточно перейти на rails 3.1)
3. не использовать анонимные классы
4. сначала создать пустой анонимный класс, а потом добавить в него нужное содержимое с помощью class_eval:

c = class.new(Base)
c.class_eval do
  # body
end

В заключение, хочу отметить, что описываемый баг — достаточно специфичен и встретится с ним далеко не каждый, но с другой стороны, зная эту особенность ruby 1.8.7 и все же нарвавшись на описываемую ситуацию можно сэкономить пару часов на дебаге.

Автор: Goganchic

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js