Classes: Encapsulation
Accessing Data
By default, instance variables are private. You cannot access @title from outside the class unless you define methods to do so.
class Book
def initialize(title)
@title = title
end
end
my_book = Book.new("The Hobbit")
puts my_book.title
Output:
undefined method 'title' for an instance of Book (NoMethodError)
To read or modify instance variables from outside the class, you need to create getter and setter methods.
The Manual Way
There are two types of methods to access instance variables:
- Getter: A method to read the variable.
- Setter: A method to change the variable (uses the
=syntax).
By convention, both getter and setter methods are named after the instance variable they expose. For example, a getter for @title is called title, while a setter is called title=.
class Book
def initialize(title)
@title = title
end
# Getter
def title
@title
end
# Setter
def title=(new_title)
@title = new_title
end
end
Examples:
-
Reading data with a Getter:
By defining a
titlemethod, we can safely read the internal@titlevariable from outside the class.class Book
def initialize(title)
@title = title
end
# Getter
def title
@title
end
end
book = Book.new("Old Title")
puts book.titleOutput:
Old Title -
Overriding the title by calling the setter method:
The
title=method allows us to reassign the internal variable.class Book
def initialize(title)
@title = title
end
# Getter
def title
@title
end
# Setter
def title=(new_title)
@title = new_title
end
end
book = Book.new("Old Title")
# Overriding
book.title = "New Title"
puts book.titleOutput:
New Title -
Handling multiple attributes:
As a class grows, you must define a separate getter and setter for every instance variable you wish to access.
class Book
def initialize(title, author)
@title = title
@author = author
end
# Getter 1
def title
@title
end
# Getter 2
def author
@author
end
# Setter 1
def title=(value)
@title = value
end
# Setter 2
def author=(value)
@author = value
end
end
book1 = Book.new("The Great Gatsby", "F. Scott Fitzgerald")
puts "#{book1.title} by #{book1.author}"Output:
The Great Gatsby by F. Scott Fitzgerald -
Shorthand syntax:
Same code as number 3, but more compact and written on a single line using semicolons.
class Book
def initialize(title, author)
@title = title
@author = author
end
# Getters
def title; @title; end
def author; @author; end
# Setters
def title=(value); @title = value; end
def author=(value); @author = value; end
end
book1 = Book.new("The Great Gatsby", "F. Scott Fitzgerald")
puts "#{book1.title} by #{book1.author}"
The Ruby Way (Attribute Accessors)
Writing out getters and setters for every attribute can quickly become tedious. To keep things clean, Ruby provides built-in shortcuts called attribute accessors to generate these methods automatically.
| Shortcut | Effect |
|---|---|
attr_reader | Creates a Getter (Read-only) |
attr_writer | Creates a Setter (Write-only) |
attr_accessor | Creates Both (Read & Write) |
Examples:
-
Using attr_accessor for full access:
This shortcut creates both the getter and setter methods for the specified attributes in a single line.
class Student
attr_accessor :first_name, :last_name, :email
def initialize(first, last, email)
@first_name = first
@last_name = last
@email = email
end
def to_s
"Full name: #{@first_name} #{@last_name}"
end
end
student1 = Student.new("John", "Smith", "john.smith@abc.com")
# Retrieve values using the generated getters
puts student1
puts student1.emailOutput:
Full name: James Dean
james.dean@abc.com -
Using attr_reader for Read-Only data:
If an attribute should be set only at creation and never changed from the outside (like an
student_id), useattr_reader.class Student
attr_accessor :first_name, :last_name, :email
attr_reader :student_id
def initialize(id, first, last, email)
@student_id = id
@first_name = first
@last_name = last
@email = email
end
end
student1 = Student.new(103, "John", "Smith", "john.smith@abc.com")
# Attempt to overwrite the ID
student1.student_id = "213"Output:
undefined method 'student_id=' for an instance of Student (NoMethodError)This returns an exception because
attr_readeronly creates a getter method. When you attempt to usestudent1.student_id = "213", Ruby looks for a setter method namedstudent_id=. Since that method was never defined, the program crashes with aNoMethodError.
Encapsulation and Instance Methods
Encapsulation means keeping an object’s data hidden and only allowing access through methods. This makes objects easier to use and helps avoid mistakes.
- Object data (instance variables) is private by default
- Methods control how data is read or changed
Instance methods are the way we interact with these objects. They belong to the specific instance and can access the object's internal data using instance variables.
Examples:
-
A Animal class with an
informationmethod:This example shows how an instance method can combine multiple private variables into a single descriptive string.
class Animal
def initialize(species, sound, diet)
@species = species
@sound = sound
@diet = diet
end
def information
"The #{@species} makes a '#{@sound}' sound and is a #{@diet}."
end
end
lion = Animal.new("Lion", "Roar", "Carnivore")
puts lion.informationOutput:
The Lion makes a 'Roar' sound and is a Carnivore.Here, the variables
@species,@sound, and@dietare private. The outside world cannot access them directly, but theinformationmethod safely exposes them in a controlled format. -
A Student class with multiple instance methods:
An object can have various instance methods, like formattting strings and performing logical checks.
class Student
def initialize(first_name, last_name, grade)
@first_name = first_name
@last_name = last_name
@grade = grade
end
# Instance method 1: Combines names
def full_name
"Full name: #{@first_name} #{@last_name}"
end
# Instance method 2: Evaluates the grade
def passing?
print "Passed: "
@grade >= 60
end
end
student = Student.new("Alex", "Smith", 85)
puts student.full_name
puts student.passing?Output:
Full name: Alex Smith
Evaluation: true
The self Keyword
self refers to the "current object." Its meaning changes depending on where it is used.
self in a Class
When used inside a class but outside any method, self refers to the class itself.
class Animal
puts "Inside Animal class, self is #{self}"
end
Output:
Inside Animal class, self is Animal
Here, self refers to the Animal class itself because it is used directly within the class body. When printed, it displays the class name. This proves that the current execution context is the class object rather than a specific instance.
self in an Instance Method
When used inside a method, self refers to the specific instance calling that method.
class Animal
def details
puts "Inside details, self is #{self}"
puts "Is it nil? #{self.nil?}"
puts "Its class is #{self.class}"
end
end
animal1 = Animal.new
animal1.details
Output:
Inside details, self is #<Animal:0x00007ebe7cf07c60>
Is it nil? false
Its class is Animal
In this case, self refers to animal1. Because we have access to self, we can easily inspect or interact with the instance from the inside.
self is essentially a reference to the current entity, whether it is a class or an instance. It allows Ruby to manage method calls and data access correctly within different contexts.
Omitting self in Instance Methods
In most cases, you don't need to write self explicitly when calling one instance method from another within the same object. Ruby automatically substitutes self behind the scenes and assumes you mean "this object."
In the example below, Ruby assumes the nil_details and class_details methods belong to the current Animal instance. This is why no explicit self is needed for normal instance method calls.
class Animal
def nil_details
puts "Object is not nil"
end
def class_details
puts "This object is a Animal class"
end
def details
nil_details # Ruby assumes self.nil_details
class_details # Ruby assumes self.class_details
end
end
Animal = Animal.new
Animal.details
Output:
Object is not nil
This object is a Animal class
Method Lookup & Implicit self
When Ruby encounters a method call inside an instance method, it follows a specific lookup process:
- It first looks for that method on the current object (
self). - If found, it runs the method as if
self.method_namewas written explicitly. - If it cannot find a matching method name, it raises a
NoMethodError.
Example of a failed lookup:
class Animal
def details
non_existing_method # Will raise an error
end
end
Output:
undefined method `non_existing_method` for #<Animal:0x00007f...>
In most cases, you can leave out self when calling instance methods. Ruby automatically assumes the current object is the receiver, which keeps your code clean.
However, self is still required in specific situations. A common example is when calling setter methods. Without self, Ruby might mistake the method call for a simple local variable assignment.