Back-end Engineering Articles

I write and talk about backend stuff like Ruby, Ruby On Rails, Databases, Testing, Architecture / Infrastructure / System Design, Cloud, DevOps, Backgroud Jobs, and more...

Twitter:
@daniel_moralesp

2019-08-30

Ruby Getters and Setters in Object-oriented Programming

At the end of the last blog post called: Methods and Attributes in Object-oriented programming in Ruby, we study attributes. Let's recap

Attributes (object information)


Just as an object has behavior (methods), objects can also have information or attributes. For example, a person can have a name, age, height, etc. These are the attributes. Think of attributes as variables that are associated with the object. 

In Ruby, we will identify attributes in a class because they begin with the @ character. For example, we can store the argument coming from the constructor inside the Person attribute. Why is this important? Because once we have an instance variable in the constructor, we can use it in any other method. Let's see how

#1. Class declaration
class Person
 #2. Constructor method called "initialize"
 def initialize(name)
   @name = name
 end
 
 # 3. Behavior or logic to apply in each instance
 def greet
   "Hello #{@name}!"
 end
end
 
#4. Instantiation
p1 = Person.new("Ana")
p2 = Person.new("Daniel")
p3 = Person.new("Matz")
 
#5. Printing out and calling "greet" class method
puts p1.greet
puts p2
puts p3.greet

Let's execute the code.


➜  blog_tutorials ruby oop.rb
Hello Ana!
#<Person:0x0000000000a6eca8>
Hello Matz!


We have just said that instance variables declared as @ can be used in any other method inside the current class. But what if we want to use it in another class? Or outside the class? Here is where we can use the getters and setters

Getters and Setters

These methods allow us to access a class's instance variable from outside the class. Getter methods are used to get the value of an instance variable, while the setter methods are used to set the value of an instance variable of some class. 

This is also called attributes visibility because the attribute @name can be just called inside Person instances, and if we want to expose it outside the class, we have to create methods to read it or edit it. 

I know this can be a bit confusing; it is one of the most complex parts to understand in an Object-oriented world, so let's see an example.

Getter

It is the method we have to create inside the class to be read outside the class. However, it's easy because we just need to make a new method and return the instance variable.

#1. Class declaration
class Person
 #2. Constructor method called "initialize"
 def initialize(name)
   @name = name
 end
 
 # 3. Behavior or logic to apply in each instance
 def greet
   "Hello #{@name}!"
 end
 
 #4. Getter method. @name can be read from the outside
 def name
   @name
 end
end
 
#5. Instantiation
p1 = Person.new("Ana")
p2 = Person.new("Daniel")
p3 = Person.new("Matz")
 
#6. Printing out and calling a class method in some of them
puts p1.greet
puts p2
puts p3.greet
 
#7. Getting the method from outside the class
p4 = Person.new("Mary")
puts p4.name

Let's execute this code.

➜  blog_tutorials ruby oop.rb
Hello Ana!
#<Person:0x000000000235b338>
Hello Matz!
Mary


As we learned previously, the instance variables (start with @) are used inside the class for other methods. However, here we are printing out the instance variable outside the class. In Step #4, We declared the method which returns the instance variable. Then, in Step #7, We created a new object called "p4," and then we called the getter method called "name." 

A bit weird, I know. And probably seems useless at this moment, but believe me: it is used a lot in Ruby OOP. For instance, what will happen if we have more instance variables in the class, for example, @age and @gender. What do you think we should do now?

We should do something like this:

#1. Class declaration
class Person
 #2. Constructor method called "initialize"
 def initialize(name, age, gender)
   @name = name
   @age = age
   @gender = gender
 end
 
 # 3. Behavior or logic to apply in each instance
 def greet
   "Hello #{@name}!"
 end
 
 #4. Getter methods. @name, @age and @gender can be read from the outside
 def name
   @name
 end
 
 def age
   @age
 end
 
 def gender
   @gender
 end
end
 
#5. Instantiation
p1 = Person.new("Ana", 25, "Female")
p2 = Person.new("Daniel", 29, "Male")
p3 = Person.new("Matz", 30, "Male")
 
#6. Printing out and calling a class method in some of them
puts p1.greet
puts p2
puts p3.greet
 
#7. Getting the method from outside the class
p4 = Person.new("Mary", 28, "Female")
puts p4.name, p4.age, p4.gender

Let's execute the code.

➜  blog_tutorials ruby oop.rb
Hello Ana!
#<Person:0x00000000027fef70>
Hello Matz!
Mary
28
Female

Here we have some interesting facts.

  • * In Step #1, we changed the constructor (initializer) to receive and save 2 new instance variables, @age, and @gender
  • * In Step#4 we created other 2 methods, "age" and "gender," and each one of them returns the corresponding instance variable
  • * In Step#5 and Step#7, we create the objects with all the new instances (name, age, gender)
  • * In Step #7 at the final line, we printed out the 3 instance variables for "Mary," who is the Person assigned to "p4."

This is how we can establish getters, but there is a better way. Let's see this code. 

#1. Class declaration
class Person
 #2. Getter methods. @name, @age and @gender can be read from the outside
 attr_reader :name, :age, :gender
 
 #3. Constructor method called "initialize"
 def initialize(name, age, gender)
   @name = name
   @age = age
   @gender = gender
 end
 
 # 4. Behavior or logic to apply in each instance
 def greet
   "Hello #{@name}!"
 end
end
 
#5. Instantiation
p1 = Person.new("Ana", 25, "Female")
p2 = Person.new("Daniel", 29, "Male")
p3 = Person.new("Matz", 30, "Male")
 
#6. Printing out and calling a class method in some of them
puts p1.greet
puts p2
puts p3.greet
 
#7. Getting the method from outside the class
p4 = Person.new("Mary", 28, "Female")
puts p4.name, p4.age, p4.gender

Let's execute the code.

➜  blog_tutorials ruby oop.rb
Hello Ana!
#<Person:0x0000000001dd30c8>
Hello Matz!
Mary
28
Female


Can you see something different?

We removed Step#4, where we had the 3 methods that returned the instance variables. This is because Ruby has a particular method to deal with getters, and with that, we don't have to create a lot of methods each time we need an instance variable outside the class. The name of these methods is accessors. In our case, the accessor is called attr_reader.

So, in Step#2, We declared the accessor method attr_reader with the 3 instance variables as "symbols." Here you can learn a bit more about Ruby Symbols. Then we deleted the 3 methods created previously in Step#4, and that's it. So, we have the same result as before, but with less code and better organization. 

Setters

This is a bit more complex (initially), but it works similar to getters. The difference with getters is that setters allow us to edit the instance variable from outside the class, and we use a syntax quite different. Let's see it.

#1. Class declaration
class Person
 #2. Getter methods. @name, @age and @gender can be read from the outside
 attr_reader :name, :age, :gender
 
 #3. Constructor method called "initialize"
 def initialize(name, age, gender)
   @name = name
   @age = age
   @gender = gender
 end
 
 # 4. Behavior or logic to apply in each instance
 def greet
   "Hello #{@name}!"
 end
 
 # 5. Setter for @name instance variable
 def name=(name)
   @name = name
 end
end
 
#6. Instantiation
p1 = Person.new("Ana", 25, "Female")
p2 = Person.new("Daniel", 29, "Male")
p3 = Person.new("Matz", 30, "Male")
 
#7. Printing out and calling a class method in some of them
puts p1.greet
puts p2
puts p3.greet
 
#8. Getting the method from outside the class
p4 = Person.new("Mary", 28, "Female")
puts p4.name, p4.age, p4.gender
 
#9. Setting a new Name (editing instance variable)
p4.name=("Maria")
puts p4.name

Let's print the result.

➜  blog_tutorials ruby oop.rb
Hello Ana!
#<Person:0x000000000282ece8>
Hello Matz!
Mary
28
Female
Maria


As always, what can you see differently?

In Step#5 we have created a new method with a weird syntax

# 5. Setter for @name instance variable
 def name=(name)
   @name = name
 end


This syntax means we're declaring a setter method, so we receive the param name and assign it to the original instance variable @name. Again, I know this is confusing, but let's check Step #8 and Step#9. First, we instantiate Person as "Mary." But in Step #9, We resigned that name to "Maria," just calling the method and passing the new parameter. 

Maybe you are asking now if we have an accessor for this because if we want to edit age and gender, we have to create another 2 methods similar to the above. Yes, ruby has the accessor method called attr_writer; let's refactor and see how it looks.

#1. Class declaration
class Person
 #2. Getter methods. @name, @age and @gender can be read from the outside
 attr_reader :name, :age, :gender
 
 #3. Setter method for name
 attr_writer :name
 
 #4. Constructor method called "initialize"
 def initialize(name, age, gender)
   @name = name
   @age = age
   @gender = gender
 end
 
 # 5. Behavior or logic to apply in each instance
 def greet
   "Hello #{@name}!"
 end
end
 
#6. Instantiation
p1 = Person.new("Ana", 25, "Female")
p2 = Person.new("Daniel", 29, "Male")
p3 = Person.new("Matz", 30, "Male")
 
#7. Printing out and calling a class method in some of them
puts p1.greet
puts p2
puts p3.greet
 
#8. Getting the method from outside the class
p4 = Person.new("Mary", 28, "Female")
puts p4.name, p4.age, p4.gender
 
#9. Setting a new Name (editing instance variable)
p4.name=("Maria")
puts p4.name

Let's print out

➜  blog_tutorials ruby oop.rb
Hello Ana!
#<Person:0x00000000026a6e70>
Hello Matz!
Mary
28
Female
Maria

And we have the same result as before. We deleted Step#5 because it contains the setter method; now, we have the accessor method in Step #3 as attr_writer. Nice!

Accessors


But what happens if we want to have the ability to read and write all three instance variables? (name, age, and gender). We have the third and last Ruby accessor method: attr_accessor. 

This accessor generates the automatic Getter & Setter method for the given item.

#1. Class declaration
class Person
 #2. Getter methods. @name, @age and @gender can be read from the outside
 attr_accessor :name, :age, :gender
 
 #3. Constructor method called "initialize"
 def initialize(name, age, gender)
   @name = name
   @age = age
   @gender = gender
 end
 
 # 4. Behavior or logic to apply in each instance
 def greet
   "Hello #{@name}!"
 end
end
 
#5. Instantiation
p1 = Person.new("Ana", 25, "Female")
p2 = Person.new("Daniel", 29, "Male")
p3 = Person.new("Matz", 30, "Male")
 
#6. Printing out and calling a class method in some of them
puts p1.greet
puts p2
puts p3.greet
 
#7. Getting the method from outside the class
p4 = Person.new("Mary", 28, "Female")
puts p4.name, p4.age, p4.gender
 
#8. Setting a new Name (editing instance variable)
p4.name=("Maria")
puts p4.name

Let’s print

➜  blog_tutorials ruby oop.rb
Hello Ana!
#<Person:0x00000000020f6e30>
Hello Matz!
Mary
28
Female
Maria

We did here to refactor Step#2 with the attr_accessor, which generates the automatic Getter & Setter method for the given items. So now we read and edit the 3 instance variables outside the class. 

Remember, there are three types of accessors in Ruby.

* attr_reader: This accessor generates the automatic Getter method for the given item.
* attr_writer: This accessor generates the automatic Setter method for the given item.
* attr_accessor: This accessor generates the automatic Getter & Setter method for the given item.

Next blog post, we'll be doing some exercises to be more precise with Ruby objects and classes and also with instance variables (getters and setters)

I hope you learned a lot from this post

Thanks for reading
Daniel Morales