One thing that puzzled me as a newbie (disclaimer: I still am) are accessors in Ruby, more commonly known as setters and getters or explicitly described as attr_reader
, attr_writer
and attr_accessor
. Now let’s dive into the code first and describe the concepts of accessors after we’re done with coding.
Initializing a Class
Let’s say we want to create a class to resemble a Person
with a name
, and finally let’s try to access that name outside the class:
# Class definition
class Person
def initialize(name)
@name = name
end
end
# Initialize class and inspect it
p1 = Person.new('Anne')
# Can't access 'name' variable outside class
puts p1.name
But it seems that we can’t access the variable @name
even though we initialized right:
# Output:
test.rb:13:in `<main>': undefined method `name' for #<Person:0x00000002278100 @name="name"> (NoMethodError)
This is because we have to define a method to access the variable inside the class.
Getters and Setters
Let’s grab our code and add two methods:
- A method to update the person’s name
- And a method to read the person’s name
# Class definition
class Person
def initialize(name)
@name = name
end
# Update 'name'
# use =() to make a method behave
# like an attribute assignment
def name=(name)
@name = name
end
# Get value of 'name'
def name
@name
end
end
# Initialize class and the value 'name'
p1 = Person.new('Anne')
puts p1.name
# Change 'name'
p1.name = 'Johnson'
puts p1.name
# Output
Anne
Johnson
Now this code works perfectly, but a person doesn’t have just one attribute like name
, they have age, height, eye color, skin color, hair color. Could you imagine writing endless methods about each attribute? Gladly we have accessors
Write and Read Accessor
Before we disclose the technical concept of accessors
let’s first try them, shall we? We’re changing our code into a more sophisticated and short one with attr_accessor
:
# Class definition
class Person
attr_accessor :name
def initialize(name)
@name = name
end
end
# Initialize class and the value 'name'
p1 = Person.new('Anne')
puts p1.name
# Change name
p1.name = 'Johnson'
puts p1.name
# Output
Anne
Johnson
What happened here? We wrote an accessor
that allow us to read and update the attribute name
in the class Person
, this way we can add more attributes like height
and weight
in a single line:
# Class definition
class Person
attr_accessor :name, :height, :weight
def initialize(name)
@name = name
end
end
# Initialize class with a name and
# add values outside the class
p1 = Person.new('Anne')
p1.height = '1.80m'
p1.weight = '180lbs'
puts "Name: #{p1.name}"
puts "Height: #{p1.height}"
puts "Weight: #{p1.weight}"
# Output
Name: Anne
Height: 1.80m
Weight: 180lbs
Write-Only Accessor
Now that’s a lot easier than to write and read each method, but what about if want to set an attribute but not read them? For example, a person can have many thoughts but no one else can read them, let’s give this person a thoughts
attribute and let’s try to access this person thoughts outside the class:
# Class definition
class Person
attr_accessor :name, :height, :weight
attr_writer :thoughts
def initialize(name)
@name = name
end
end
# Initialize class with a name and
# add values outside the class
p1 = Person.new('Anne')
p1.height = '1.80m'
p1.weight = '180lbs'
# Set a thought and inspect the class
p1.thoughts = 'pizza <3'
puts p1.inspect
puts "Name: #{p1.name}"
puts "Height: #{p1.height}"
puts "Weight: #{p1.weight}"
# Try to access that thought
puts "#{p1.name} is thinking about: #{p1.thoughts}"
# Output
#<Person:0x00000000e5f538 @name="Anne", @height="1.80 meters", @weight="180 lbs", @thoughts="pizza <3">
Name: Anne
Height: 1.80 meters
Weight: 180 lbs
test.rb:26:in `<main>': undefined method `thoughts' for #<Person:0x00000000e5f538> (NoMethodError)
See? We are able to set a value on the thought
attribute, but outside the class we are unable to read it (line 26)
Read-Only Accessor
Now, a person have a lot of things that they can’t change… not by conventional means at least. Let’s add a read-only attribute to this person with attr_reader
, like eye_color
in line 5 and initialize it:
# Class definition
class Person
attr_accessor :name, :height, :weight
attr_writer :thoughts
# read-only accessor
attr_reader :eye_color
# initialize eye_color
def initialize(name, eye_color)
@name = name
@eye_color = eye_color
end
end
# Initialize class with a name and
# add values outside the class
p1 = Person.new('Anne', 'Green')
p1.height = '1.80m'
p1.weight = '180lbs'
p1.thoughts = 'pizza <3'
puts "Name: #{p1.name}"
puts "Height: #{p1.height}"
puts "Weight: #{p1.weight}"
# Read eye color
puts "Eye Color: #{p1.eye_color}"
# Output
Name: Anne
Height: 1.80m
Weight: 180lbs
Eye Color: Green
As you can see there’s no problem in initializing and accessing the eye_color
attribute, but as soon as we try to change it with p1.eye_color = 'red'
, we can expect the following response:
test.rb:30:in `<main>': undefined method `eye_color=' for #<Person:0x000000019defa8> (NoMethodError)
That, of course, is because we set the accessor as read only.
Conclusion and Technical Definition
I cannot describe better what an accessor is more than the definition found in the official Ruby user’s guide:
An object’s instance variables are its attributes, the things that generally distinguish it from other objects of the same class. It is important to be able to write and read these attributes; doing so requires writing methods called attribute accessors.
Basically, attr_accessor
, attr_writer
and attr_reader
is a short way to define and set class variables without having to define the methods to access and read them outside the class, with:
attr_accessor
we are able to read and write values outside the class.attr_writer
allow us to write values without being able to read them outside the class.- and with
attr_reader
we can initialize and read the class attributes without being able to reassign it.
You can read more about accessors and how useful they are in the following documentation: