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
endThen 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
endBut, 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
endNow, 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
# >> :nameNow 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
endIf 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
endThis 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
endNow 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"
# >> 2So 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
endAnd 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
endNow we can add attribute readers to our Pet class.
class Pet
attribute_writer :name
attribute_reader :name
attribute_writer :age
attribute_reader :age
endAnd we can try them out.
pet.name # => "Mikachu"
pet.age # => 2Attribute 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
endAnd refactor our code to use our new accessors.
class Pet
attribute_accessor :name
attribute_accessor :age
endWhich deliver the expected results.
pet = Pet.new
pet.name = "Mikachu"
pet.age = 2
pet.name # => "Mikachu"
pet.age # => 2Conclusion
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.