#07Attribute reader, writer and accessor

Subscribe to our episode notifications

* indicates required

Intuit Mailchimp

Today we're going to talk about three methods you've probably used: attr_reader, attr_writer and attr_accessor.

Lets start by showing off how they work. We'll create a Pet class and add an attribute accessor for the name attribute.

class Pet
  attr_accessor :name
end

Then we create an instance of this class.

The attribute accessor created the name= method for us, so lets assign a name to our pet.

pet = Pet.new
pet.name = "Mikachu"

Now that our pet has a name, we can retrieve it calling the name method.

pet.name # => "Mikachu"

Finally, if we inspect this object, we can see that an instance variable called @name has been defined.

pet
# => #<Pet:0x00007f0344424ca0 @name="Mikachu">

This is how we use attribute accessors in Ruby.

Ruby also provides an attribute reader, which only creates the name method and an attribute writer, which only creates the name= method independently.

class Pet
  attr_reader :name
  attr_writer :name
end

But, how do they work behind the scenes?

Attribute writer

To understand how this all works, we'll create our own reader, writer and accessor.

Lets start with the writer.

We need to open up the Object class in order for our writer to be available on every object in our system.

We'll create a class method called attribute_writer that receives the attribute name and, for now, just prints it out

class Object
  def self.attribute_writer(attribute_name)
    p attribute_name
  end
end

Now, we can create our Pet class and create the name attribute using our new method.

And when we execute this code, we can see that it printed out the :name symbol.

class Pet
  attribute_writer :name
end

# >> :name

Now that we have the attribute_writer method defined at the class level, we need it to define the name= method.

At a first glance, we could do this with def, since that's the standard way of creating methods in Ruby.

class Object
  def self.attribute_writer(attribute_name)
    def name=(value)
      p value
    end
  end
end

If we do it this way, it works, but it gives us no flexibility on the method's name.

pet = Pet.new
pet.name = "Mikachu"

# >> "Mikachu"

So we switch to define_method, which receives a string that represents the method name as an argument and a block to which we yield the method arguments and where we define the method's body.

class Object
  def self.attribute_writer(attribute_name)
    define_method("name=") do |value|
      p value
    end
  end
end

This does the same thing as before, but now we're receiving a string that we can manipulate as we please, for example, we can name it after the attribute name.

class Object
  def self.attribute_writer(attribute_name)
    define_method("#{attribute_name}=") do |value|
      p value
    end
  end
end

Now we can define any attribute writer we want. And they work as expected.

class Pet
  attribute_writer :name
  attribute_writer :age
end

pet = Pet.new
pet.name = "Mikachu"
pet.age = 2

pet # => #<Pet:0x00007fa6c65f2fe8>

# >> "Mikachu"
# >> 2

So far, we have our attribute writer method, but it doesn't actually set a variable.

We can do so, by calling the instance_variable_set method with the name of the variable as the first argument and the value we want to set as the second.

class Object
  def self.attribute_writer(attribute_name)
    define_method(:"#{attribute_name}=") do |value|
      instance_variable_set("@#{attribute_name}", value)
    end
  end
end

And now, when we run this code, we can see that our attribute writer defines the name and age instance variables, as we intended.

class Pet
  attribute_writer :name
  attribute_writer :age
end

pet = Pet.new
pet.name = "Mikachu"
pet.age = 2

pet # => #<Pet:0x00007eff996d31f8 @name="Mikachu", @age=2>

Attribute reader

Now that we are able to set an attribute in our class, we can start writing the attribute reader to retrieve it.

The same as before, we start by creating the attribute_reader class method that receives the attribute name as an argument.

Then we dynamically define a method named after the attribute. Note that this time we don't need to use interpolation, since the method name is exactly what we get as an argument.

Finally, we fetch the instance variable named as our attibute.

def self.attribute_reader(attribute_name)
  define_method(attribute_name) do
    instance_variable_get("@#{attribute_name}")
  end
end

Now we can add attribute readers to our Pet class.

class Pet
  attribute_writer :name
  attribute_reader :name
  attribute_writer :age
  attribute_reader :age
end

And we can try them out.

pet.name # => "Mikachu"
pet.age # => 2

Attribute accessor

Now that we know how to create attribute readers and writers, we are in condition to write our attribute accessor.

As we've been doing, we start by writing the class method.

But, since we've already implemented all the functionality we need, we can implement it by utilizing our attribute writer and reader.

class Object
  # ...

  def self.attribute_accessor(attribute_name)
    attribute_writer(attribute_name)
    attribute_reader(attribute_name)
  end
end

And refactor our code to use our new accessors.

class Pet
  attribute_accessor :name
  attribute_accessor :age
end

Which deliver the expected results.

pet = Pet.new
pet.name = "Mikachu"
pet.age = 2

pet.name # => "Mikachu"
pet.age # => 2

Conclusion

We often rely on attribute readers and writers, and it's easy to consider it just "Ruby magic".

Understanding how they work under the hood not only deepens our grasp of the language but also equips us to create our own custom tools.

I hope you enjoyed this episode and I'll see you on the next one.

Subscribe to our episode notifications

* indicates required

Intuit Mailchimp

Comments