📜 ⬆️ ⬇️

The order of callbacks for inheritance

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

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


Here, two sub-modules are created within the current module to separate the instance and the class methods. When the MyModule module is “mixed in” in the 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 performed during inheritance.

 class Ancestor def self.inherited(successor) end end class Successor < Ancestor end 


In my opinion, in some situations this construct 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" class Base class_inheritable_accessor :color end Base.color = "red" class Ancestor < Base end Ancestor.color # => "red" Ancestor.color = "green" Base.color # => "red" 


Conveniently? Full True, in recent versions of rails, the inheritable_attributes module has been deprecated and replaced with class_attribute.

As soon as the notion 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:

 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 


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

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


When porting the well-known gem active_attr, I encountered the following feature: all the specs run normally at 1.99 in ruby, but just ask rvm to use good old ruby ​​1.8.7 (or ree) - so half of the tests start to fall off for no apparent reason (I’ll note that before my commits, there was no support for rails 3.0 and therefore all the specs worked out correctly both on ruby ​​branches 1.9 and on ruby ​​branches 1.8)

As it turned out, the reason for the following features of 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 


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

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

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


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

Source: https://habr.com/ru/post/141124/


All Articles