Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What happens when attr_accessor is within a class method?

So this came up to my mind and wonder what happens when some thing below is done.

 class Test 
   def self.abc
     attr_accessor :John 
   end
 end

 object = Test.new
 puts "before calling class method abc:  #{object.class.instance_methods(false)}"
 Test.abc
 puts "after calling class method abc:   #{object.class.instance_methods(false)}"

Here what I checked was, do the getter and setter methods get created in this way. And if so, are those instance methods or class methods. First I create a new object and then I view what are the instance methods of that object. After that in next line I run the class method abc and then again, I check for instance methods of object. At that time only I can see the two methods John and John=. How does this happen ? How come a running of a class method dynamically add methods to already created objects?. Can somebody please explain me this.

The output of the code was:

before calling class method abc:  []
after calling class method abc:   [:John, :John=]
like image 755
Ashan Priyadarshana Avatar asked Feb 01 '17 18:02

Ashan Priyadarshana


2 Answers

When in a class method, self is the class itself. Therefore, the following are equivalent in what they ultimately do:

class Test
  def self.abc
    # `self` in this context is Test.
    # This sends the message `:attr_accessor, :john` to `Test`
    attr_accessor :john
  end
end

class Test
  # `self` in this context is Test.
  # This sends the message `:attr_accessor, :john` to `Test`
  attr_accessor :john
end

However, as you noted, Test::abc isn't executed when the class is parsed, so attr_accessor isn't called, and the instance methods aren't added. It is perfectly valid to do this at runtime, and in fact, is the basis of much of the metaprogramming performed in Rails.

Typically, if you expect to add accessors via a class method, you might call that class method after definition but still during class declaration:

class Test
  def self.abc
    attr_accessor :john
  end

  abc
end

This will actually run, and properly declare the accessors on the class!

As to your question:

How come a running of a class method dynamically add methods to already created objects?

This is because instantiating a class doesn't instantiate a "snapshot" of the class at the time of instantiation - it creates an object which delegates much of its functionality (including discovery of the instance methods on it) to the class associated with it. Note that it is possible to define new methods on an instance which don't extend back to the class, too:

class Test
  attr_accessor :foo
end

t1 = Test.new
t2 = Test.new

Test.send(:define_method, :bar) {}
puts t1.respond_to? :foo  # => true
puts t2.respond_to? :foo  # => true

puts t1.respond_to? :bar  # => true
puts t2.respond_to? :bar  # => true

t1.define_singleton_method(:baz) {}

puts t1.respond_to? :baz  # => true
puts t2.respond_to? :baz  # => false
like image 197
Chris Heald Avatar answered Sep 17 '22 23:09

Chris Heald


Firstly, note that in place of

object = Test.new
object.class.instance_methods(false)

you could simply write

Test.instance_methods(false)

so let's simplify:

puts "before calling class method abc:  #{Test.instance_methods(false)}"
  # (prints) []
Test.abc
puts "after calling class method abc:   #{Test.instance_methods(false)}"
  # (prints) [:John, :John=]

There's a short explanation of this behaviour, but also a longer one.

Short Explanation

Test.method(:attr_accessor)
  #=> #<Method: Class(Module)#attr_accessor> 

This return value, together with the fact that

Test.class #=> Class

tells us that attr_accessor is an instance_method of Class, and therefore a method of Test. Let's confirm.

Class.instance_methods.include?(:attr_accessor)
  #=> false

Whoops! (You were expecting => true?). That can only mean that attr_accessor is a protected or private instance method:

Class.protected_instance_methods.include?(:attr_accessor)
  #=> false

Class.private_instance_methods.include?(:attr_accessor)
  #=> true

Therefore

Class.private_instance_methods.include?(:attr_accessor)
  #=> true

So attr_accessor is simply a private instance method of Test's class, Class, making it a private method of Test. The expression (from earlier)

Test.method(:attr_accessor)
  #=> #<Method: Class(Module)#attr_accessor> 

also tells us that attr_accessor is defined in Class's superclass Module.

Class.superclass
  #=> Module
Class.ancestors
  #=> [Class, Module, Object, Kernel, BasicObject] 
Class.instance_method(:attr_accessor).owner
  #=> Module 
Test.method(:attr_accessor).owner
  #=> Module 

Longer Explanation

The method Module#attr_accessor can be made available to the class Test in one of two ways. This is the essence of Ruby's object model. (I have sidestepped the place of singleton class methods.)

1. Test inherits the method from its superclass

Test.superclass
  #=> Object
Test.ancestors
  #=> [Test, Object, Kernel, BasicObject]

Let's see.

Object.method(:attr_accessor)
  #=> #<Method: Class(Module)#attr_accessor> 
Object.public_methods.include?(:attr_accessor)
  #=> false 
Object.private_methods.include?(:attr_accessor)
  #=> true

2. attr_accessor is an instance method of Test's class

Test.class
  #=> Class

Class.private_instance_methods.include?(:attr_accessor)
  #=> true

So Test both inherits attr_accessor from its superclass and has it available as an instance method of its class. This duality is often explained with a diagram such as this one.

Ruby first checks Test's class, Class, for the instance method attr_accessor. where it will find it. (Had she not found it there she'd then look for the method (not instance method) in the superclass.)

like image 38
Cary Swoveland Avatar answered Sep 18 '22 23:09

Cary Swoveland