Inheritance of callbacks

    Ruby is a very interesting language. One of its features is the ability to perform specified functions when adding a module to a class. A standard example is as follows:

    moduleMyModulemoduleInstanceMethodsendmoduleClassMethodsenddefself.included(base)
        base.include(InstanceMethods)
        base.extend(ClassMethods)
      endend


    Here, two sub-modules are created within the current module to separate instance methods and class methods. When "mixing" the MyModule module into a class, the included function is executed, which adds the necessary class methods and methods of the class objects.

    Not so long ago, I discovered another similar function that is executed during inheritance.

    classAncestordefself.inherited(successor)endendclassSuccessor < Ancestorend


    In my opinion, in some situations this construction can be very convenient, for example, in Ruby on Rails version 3.0 and later there is an InheritableAttributes module that allows you to copy class attributes during inheritance. Here is a simple example of using this module:

    require"active_support/core_ext/class/inheritable_attributes"classBase
      class_inheritable_accessor :colorend
    Base.color = "red"classAncestor < Baseend
    Ancestor.color # => "red"
    Ancestor.color = "green"
    Base.color # => "red"


    Conveniently? Quite. True, in recent versions of rails, the inheritable_attributes module was deprecated and was replaced by class_attribute.

    As soon as the concept of callback is introduced, the question immediately arises as to when this callback will be executed: before the eval of the class body or after. Check:

    classBasedefself.inherited(m)
        puts "Hello from inherited callback"endendclassNamedClass < Base
      puts "Hello from class body"end# Output:# Hello from inherited callback# Hello from class body


    Those. callback is executed before the class body eval. And everything would be fine, if not one “but”: in ruby, along with the named classes, there are also anonymous classes, which are declared like this:

    anonymous_class = Class.new(Base) do# bodyend


    Porting the well-known gem active_attr, I came across the following feature: all specs run fine in ruby ​​1.9 when running in ruby, but if you just ask rvm to use the good old ruby ​​1.8.7 (or ree), half of the tests begin to fall off for no apparent reason (I note that prior to my commits, there was no rails 3.0 support, and therefore all specs worked correctly on both ruby ​​branch 1.9 and ruby ​​branch 1.8)

    As it turned out, the reason for the following feature of ruby ​​1.8.7 is:

    classBasedefself.inherited(m)
        puts "--> Hello from inherited callback"endend
    puts "declare named class"classNamedClass < 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


    In ruby ​​1.8.7, for an anonymous class, the body is first eval- ed, and then callback inherited is executed. Thus, the InheritableAttributes module, which expects an empty class, encounters some methods and does not work correctly.

    How can I solve this problem?
    1. switch to ruby ​​1.9
    2. do not use functionality based on callback inherited (if this functionality is not used explicitly, but only within rails, then just switch to rails 3.1)
    3. do not use anonymous classes
    4. first create an empty anonymous class , and then add the desired content to it using class_eval:

    c = class.new(Base)
    c.class_eval do# bodyend


    In conclusion, I want to note that the described bug is quite specific and not everyone will encounter it, but on the other hand, knowing this feature of ruby ​​1.8.7 and still running into the described situation, you can save a couple of hours on debug.

    Also popular now: