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