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 Bookdef initialize(title)@title = titleend# Getterdef title@titleendendbook = 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 Bookdef initialize(title)@title = titleend# Getterdef title@titleend# Setterdef title=(new_title)@title = new_titleendendbook = Book.new("Old Title")# Overridingbook.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 Bookdef initialize(title, author)@title = title@author = authorend# Getter 1def title@titleend# Getter 2def author@authorend# Setter 1def title=(value)@title = valueend# Setter 2def author=(value)@author = valueendendbook1 = 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 Bookdef initialize(title, author)@title = title@author = authorend# Gettersdef title; @title; enddef author; @author; end# Settersdef title=(value); @title = value; enddef author=(value); @author = value; endendbook1 = 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 Studentattr_accessor :first_name, :last_name, :emaildef initialize(first, last, email)@first_name = first@last_name = last@email = emailenddef to_s"Full name: #{@first_name} #{@last_name}"endendstudent1 = Student.new("John", "Smith", "john.smith@abc.com")# Retrieve values using the generated gettersputs student1puts student1.emailOutput:
Full name: James Deanjames.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 Studentattr_accessor :first_name, :last_name, :emailattr_reader :student_iddef initialize(id, first, last, email)@student_id = id@first_name = first@last_name = last@email = emailendendstudent1 = Student.new(103, "John", "Smith", "john.smith@abc.com")# Attempt to overwrite the IDstudent1.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 Animaldef initialize(species, sound, diet)@species = species@sound = sound@diet = dietenddef information"The #{@species} makes a '#{@sound}' sound and is a #{@diet}."endendlion = 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 Studentdef initialize(first_name, last_name, grade)@first_name = first_name@last_name = last_name@grade = gradeend# Instance method 1: Combines namesdef full_name"Full name: #{@first_name} #{@last_name}"end# Instance method 2: Evaluates the gradedef passing?print "Passed: "@grade >= 60endendstudent = Student.new("Alex", "Smith", 85)puts student.full_nameputs student.passing?Output:
Full name: Alex SmithEvaluation: 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.