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.new
pet = "Mikachu" pet.name =
Now that our pet has a name, we can retrieve it calling the name
method.
# => "Mikachu" pet.name
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_nameend
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
:name
attribute_writer 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 valueend
end
end
If we do it this way, it works, but it gives us no flexibility on the method's name.
Pet.new
pet = "Mikachu"
pet.name =
# >> "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)
"name=") do |value|
define_method(
p valueend
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)
"#{attribute_name}=") do |value|
define_method(
p valueend
end
end
Now we can define any attribute writer we want. And they work as expected.
class Pet
:name
attribute_writer :age
attribute_writer end
Pet.new
pet = "Mikachu"
pet.name = 2
pet.age =
# => #<Pet:0x00007fa6c65f2fe8>
pet
# >> "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)
"#{attribute_name}=") do |value|
define_method(:"@#{attribute_name}", value)
instance_variable_set(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
:name
attribute_writer :age
attribute_writer end
Pet.new
pet = "Mikachu"
pet.name = 2
pet.age =
# => #<Pet:0x00007eff996d31f8 @name="Mikachu", @age=2> pet
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)
do
define_method(attribute_name) "@#{attribute_name}")
instance_variable_get(end
end
Now we can add attribute readers to our Pet
class.
class Pet
:name
attribute_writer :name
attribute_reader :age
attribute_writer :age
attribute_reader end
And we can try them out.
# => "Mikachu"
pet.name # => 2 pet.age
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
:name
attribute_accessor :age
attribute_accessor end
Which deliver the expected results.
Pet.new
pet = "Mikachu"
pet.name = 2
pet.age =
# => "Mikachu"
pet.name # => 2 pet.age
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.