How do attr_accessors in Ruby work?

What is attr_accessor?

The attr_accessor automatically creates getter and setter methods for instance variables. The way it generates the new methods for the instance variables is a manifestation of “meta”-programming which we can recreate using define_method. It calls this method once for the setter (attr_writer) and once for the getter (attr_reader).

 

These two examples have the exact same behavior, because Ruby lets you omit parentheses around parameters to beautify your code:

class Dog
  attr_reader :name 
end 
class Dog
  attr_accessor(:name)
end

Making attr_reader

Instead of trying to make both the reader and the writer at once, it might be easier to create the getter method first. We want the generated methods to apply to all instances of the class, but if we called define_method at the instance level how would we know what argument to pass it? Fortunately, if you call define_method in a class method it does the right thing and creates the new method on instances of the class.

class Dog
  def self.getter(instance_var)
    define_method("#{instance_var}".to_sym) do
      instance_variable_get("@#{instance_var}".to_sym)
    end
  end
end

Notice that this uses a baked in method on the Object class, instance_variable_get to retrieve the value of interest.  Now we can monkey-patch Dog to fetch 😉 the value of an instance variable.

class Dog
  getter :nickname
  def initialize
    @nickname = 'doggy'
  end
end

Making a new instance and checking its nickname caused desired behavior. Yay!

spot = Dog.new
spot.nickname #=> 'doggy'

Making attr_writer 

This is almost identical, however, we need to include an equal sign in the method name and an argument in the define_method block.

def self.setter(instance_var)
    define_method("#{instance_var}=".to_sym) do |arg|
      instance_variable_set("@#{instance_var}".to_sym, arg)
    end
  end
end

As you may have guessed, there is also a built-in method on the Object class known as instance_variable_set. 

Putting it all together 

Now with the proof of concept, we can combine the two to create our own version of attr_accessor:

def self.my_attr_accessor(*names)
    names.each do |instance_var|
      define_method("#{instance_var}".to_sym) do
        instance_variable_get("@#{instance_var}".to_sym)
      end
      define_method("#{instance_var}=".to_sym) do |arg|
        instance_variable_set("@#{instance_var}".to_sym, arg)
      end
    end
  end
end

 

 

Leave a Reply

Your email address will not be published. Required fields are marked *