As a rule of thumb, we define methods in our Ruby class and consider their accessibility. Often, we declare them public
and private
. In exceptional cases, we set them protected
.
As protected methods are exceptionally used, remembering and recalling their use cases is quite difficult when encountering them in the source code. I don’t feel confident about adding a protected method to the class unless I have dug deep into its definition and implementation. Honestly, it’s over and over again! Hopefully, I’m not alone 🙂
I write this post as a reference to return one day to review how to use protected methods in Ruby quickly.
Before we jump straight to protected methods, it is important to understand the basics of public and private methods. Let’s dive deep!
Understanding Public and Private Methods
Ruby offers public methods by default. However, there is an exception to the initialize
method, which is always private. Public methods can be invoked from anywhere inside and outside the scope of the class without access controls.
On the other hand, private methods can only be called inside the scope of the class and by its subclasses. The characteristic of the private method is that they can’t be called with an explicit receiver. We cannot specify an object that will invoke the method.
Let’s look at BankAccount
example demonstrating the usage of the public and private methods.
class BankAccount
attr_accessor :balance
def initialize(initial_balance = 0)
@balance = initial_balance
end
def deposit(amount)
@balance += amount
end
def withdraw(amount)
if amount <= balance
@balance -= amount
else
puts "Insufficient balance."
end
end
def transfer(amount, other)
unless amount <= balance
puts "Transfer failed. Insufficient balance."
return
end
withdraw(amount)
other.deposit(amount)
puts "Transferred successfully!"
end
end
# Usage
account1 = BankAccount.new(1000)
account2 = BankAccount.new(500)
puts "Account 1 Balance: $#{account1.balance}"
puts "Account 2 Balance: $#{account2.balance}"
account1.transfer(200, account2)
puts "After transfer:"
puts "Account 1 Balance: $#{account1.balance}"
puts "Account 2 Balance: $#{account2.balance}"
BankAccount
has everything as public from the balance
attribute to the transfer
method.
As you can see, there is no restriction on calling the public methods from outside the scope of the class by explicit objects such as account1.balance
, account2.balance
, and account1.transfer
, and inside the class within the context of transfer
method like withdraw(amount)
and other.deposit(amount)
.
More usage can be that we can deposit money toward account1
and withdraw some from it as well. For instance, account1.deposit(300)
and account1.withdraw(100)
.
What if BankAccount wants to hide the raw balance from outside of the world?
The implementation is to set the attribute balance private. Ruby 3.
private attr_accessor :balance
Now when you attempt to access the available balance account1.balance
like above, you will get NoMethodError
. But, you can call it directly in the scope of the class similarly in deposit
, withdraw
, and transfer
.
Using Protected Methods
Similar to private methods, protected methods prevent outside attempted access explicitly with a receiver. They can only be invoked from within the implementation of a class or its subclasses.
The main difference is that you can explicitly invoke a protected method on any object of the same class. With protected methods, objects can access the internal state of other objects of the same class.
Let’s go through BankAccount
class again.
class BankAccount
def initialize(initial_balance = 0, active = true)
@balance = initial_balance
@active = active
end
def deposit(amount)
@balance += amount
end
def withdraw(amount)
if amount <= @balance
@balance -= amount
else
puts "Insufficient balance."
end
end
def transfer(amount, other)
unless other.active?
puts "Transfer failed! Target account is inactive."
return
end
unless amount <= balance
puts "Transfer failed! Insufficient balance."
return
end
unless greater_balance_than?(other)
puts "Transfer failed! Other account balance is sufficient."
return
end
perform_transfer(amount, other)
end
def to_s
status_str = active? ? "Account is active." : 'Account is inactive.'
balance_str = balance.zero? ? "Current balance is empty." : "Current balance is positive."
"#{status_str} #{balance_str}"
end
protected
attr_accessor :balance, :active
def active?
active
end
private
def greater_balance_than?(other)
return balance > other.balance
end
def perform_transfer(amount, other)
withdraw(amount)
other.deposit(amount)
puts "Transferred successfully #{amount} USD to other account."
end
end
# Example usage:
account1 = BankAccount.new(100)
account2 = BankAccount.new(50)
account1.transfer(20, account2)
puts "Bank account 1: #{account1.to_s}"
puts "Bank account 2: #{account2.to_s}"
account1.withdraw(80)
puts "Bank account 1: #{account1.to_s}"
This revision introduces protected attributes: balance
and active
, and active? method. The protected method active?
behaves as an alias of the attribute active
, which directly returns a boolean value of the attribute active.
If you try to explicitly call either account1.balance
or account1.active?
it will raise NoMethodError
.
However, it runs in the case of other.active?
in transfer
method and other.balance
invoked in greater_balance_than?
method.
other
is an object of the BankAccount class receiving from account1.transfer(20, account2)
– it refers to account2
instance in this context – so that it can access its internal state.
This does not apply to private attributes and methods. When balance
and active?
are private, you call them the same way as being protected it will raise NoMethodError
.
def transfer(amount, other)
unless other.active?
puts "Transfer failed! Target account is inactive."
return
end
...
unless greater_balance_than?(other)
puts "Transfer failed! Other account balance is sufficient."
return
end
end
private
attr_accessor :balance, :active
def active?
active
end
def greater_balance_than?(other)
return balance > other.balance
end
Invocation on other.active?
in transfer method, and on other.balance
in greater_balance_than?
method will raise NoMethodError
.
Usage with self
In this context, self
refers to the current object within the class context. Upon calling account1.transfter(20, account2
), self
is set to account1
which is an instance of the BankAccount
class. self
can access all kinds of methods, such as public, private (Ruby version 3), and protected methods.
Let’s modify our BankAccount class using self
written in Ruby version 3:
class BankAccount
attr_accessor :balance, :active
private :active
protected :balance
def initialize(initial_balance = 0, active = true)
self.balance = initial_balance
self.active = active
end
def deposit(amount)
self.balance += amount
end
def withdraw(amount)
if amount <= self.balance
self.balance -= amount
else
puts "Insufficient balance."
end
end
def transfer(amount, other)
unless other.active?
puts "Transfer failed! Target account is inactive."
return
end
unless amount <= self.balance
puts "Transfer failed! Insufficient balance."
return
end
unless self.greater_balance_than?(other)
puts "Transfer failed! Other account balance is sufficient."
return
end
self.perform_transfer(amount, other)
end
def to_s
status_str = self.active? ? "Account is active." : 'Account is inactive.'
balance_str = self.balance.zero? ? "Current balance is empty." : "Current balance is #{self.balance} USD."
"#{status_str} #{balance_str}"
end
protected
def active?
self.active
end
private
def greater_balance_than?(other)
return self.balance > other.balance
end
def perform_transfer(amount, other)
self.withdraw(amount)
other.deposit(amount)
puts "Transferred successfully #{amount} USD to other account."
end
end
# Example usage:
account1 = BankAccount.new(100)
account2 = BankAccount.new(50)
account1.transfer(20, account2)
puts "Bank account 1: #{account1.to_s}"
puts "Bank account 2: #{account2.to_s}"
# Output
# Transferred successfully 20 USD to other account.
# Bank account 1: Account is active. Current balance is 80 USD.
# Bank account 2: Account is active. Current balance is 70 USD.
Conclusion
Ruby’s protected methods are useful in some ways so we should understand when to use them. They come into place when you intend to restrict access to an object’s internal states from the outside world and share those only with other objects.
I hope this summary explanation gives you a clue about Ruby’s protected methods. In case you have a comment and want to add something don’t hesitate!
Happy Digging For Ruby!!! 🙂
Leave a Reply