DEV Community

Sara Alhaddadi
Sara Alhaddadi

Posted on • Updated on

Ruby OOP review session (Part 1)

let's review the main concepts in object oriented programming (OOP) in Ruby πŸ“– πŸ’Ž


  • In Ruby, everything is an object! anything that can be said to have a value is an object , that includes numbers, strings, arrays, and even classes and modules
'hi'.class # => String

1.class #=> Integer
Enter fullscreen mode Exit fullscreen mode

Create class

  • class is a template of objects
  • a class definition is a region of code between the keywords class and end
  • class name : CamelCase naming convention as class/module name must be CONSTANT
class Employee
  # class code live here
end
Enter fullscreen mode Exit fullscreen mode

Create objects (instantiation)

  • use new class method to create object
employee = Employee.new
Enter fullscreen mode Exit fullscreen mode

initialize method

  • When you call new to create a new object, Ruby calls that object's initialize method as one step in create the object
  • any parameter passed to new will pass to initialize
  • this will help us to execute and code that we need in create object like set the state or data of these objects
class Employee
  def initialize
    puts "This object was initialized!"
  end
end

employee = Employee.new #=> This object was initialized!
Enter fullscreen mode Exit fullscreen mode

Work with Instances

Instance Variables

  • store data of instance (something instance can have) as each instance has it is own version of this variable
  • it has the @ symbol in front of it
  • It is a variable that exists as long as the object instance exists
  • From outside the object, instance variables cannot be altered or even observed (i.e., ruby's instance variables are never public).
  • As with globals, instance variables have the nil value until they are initialized.
class Employee
  def initialize(name)
    @name = name
  end
end

employee1 = Employee.new("Sara")
employee2 = Employee.new("Khalid")
Enter fullscreen mode Exit fullscreen mode

Instance Methods

  • define behavior of instance (something instance can do)
  • methods that objects can call
class Employee
  def initialize(name)
    @name = name
  end

  def say_hi
    "Hi, I am #{@name}"
  end
end

employee1 = Employee.new("Sara")
employee2 = Employee.new("Khalid")

employee1.say_hi # => "Hi, I am Sara"
employee2.say_hi # => "Hi, I am Khalid"
Enter fullscreen mode Exit fullscreen mode

Accessor Methods

  • in the above example we can not access instance variable name outside the class.
employee1.name # => NoMethodError (undefined method `name' for #<Employee:0x0000561c18d842c8 @name="Sara">)
Enter fullscreen mode Exit fullscreen mode
  • NoMethodError means that we called a method that doesn't exist or is unavailable to the object.
  • If we want to access the instance variable, we have to create a method that will deal with it.
  • getter method : method return the value (get)
  • setter method : method change the value (set), Ruby syntactical sugar to use def method_name=() that will help us to use assignment to call the setter method.
  • as a convention, Rubyists typically name those getter and setter methods using the same name as the instance variable.
class Employee
  def initialize(name)
    @name = name
  end

  def name
    @name
  end

  # normal method to set the value
  def set_name(new_name)
    @name = new_name
  end

  # syntactical sugar 
  def name=(new_name)
    @name = new_name
  end
end

employee1 = Employee.new("Sara")
employee2 = Employee.new("Khalid")

puts employee1.name # Sara
puts employee2.name # Khalid

employee1.set_name("new name")
puts employee1.name

employee2.name=("Omer")
puts employee2.name # Omer

employee2.name = "Ali"
puts employee2.name # Ali
Enter fullscreen mode Exit fullscreen mode
  • Syntactical sugar setter methods always return the value that is passed in as an argument and ignore the return value of method, because We defined the value of the assignment as the value of the right hand expression, not the return value from the assigning method
class Employee
  def initialize(name)
    @name = name
  end

  def name
    @name
  end

  def name=(new_name)
    @name = new_name
    "Hi Sara this value will ignored"
  end
end

employee1 = Employee.new("Sara")

# call the method as assignments will not return the value from the method
# all other assignments evaluate to the right-hand side in Ruby
employee1.name = "Ali" # => "Ali"

# call the method with send will return the last value
employee1.public_send(:name=, "Ali") # => "Hi Sara this value will ignored"
Enter fullscreen mode Exit fullscreen mode

attr_accessor , attr_reader ,attr_writer

  • help us define more getters and setters
  • attr_reader : getter method only allows you to retrieve the instance variable
  • attr_writer : setter method only allows you to set the instance variable
  • attr_accessor: method takes a symbol as an argument, which it uses to create the method name for the getter and setter methods
class Employee
  attr_accessor :name, :age

  def initialize(name, age)
    @name = name
    @age = age
  end
end

employee = Employee.new("Sara", 20)

employee.name = "Omer"
puts employee.name # Omer

employee.age = 15
puts employee.age # 15
Enter fullscreen mode Exit fullscreen mode
  • removing the @ symbol, we're now calling the instance method, rather than the instance variable as it's generally a good idea because by this we can limit the access to these variables to certain conditions or logic


Work with classes

Class Methods

  • Class level methods, Class methods are methods we can call directly on the class itself, without having to instantiate any objects
  • defining a class method, we prepend the method name with the reserved word self or class name
  • use self is better as you do not need to change the methods name if you will change the class name
class Employee
  def Employee.what_am_i?
    "I am #{self} class"
  end

  def self.call
    "#{self} is calling: " + what_am_i?
  end
end

puts Employee.what_am_i? # I am Employee class

puts Employee.call  # Employee is calling: I am Employee class
Enter fullscreen mode Exit fullscreen mode

Class Variables

  • Class level data shared between instances of classes
  • Class variables are created using two @ symbols like so: @@
  • same as instance variable we can not access class variable from outside the class
class Employee
  @@number_of_employees = 0

  def initialize
    @@number_of_employees += 1
    puts "Employee number #{@@number_of_employees} created successfully"
  end
end

Employee.new # Employee number 1 created successfully
Employee.new # Employee number 2 created successfully

puts Employee.number_of_employees # => NoMethodError (undefined method `number_of_employees' for Employee:Class)
Enter fullscreen mode Exit fullscreen mode

Class reader / writer methods

  • same as instance reader and writer (getter and setter ) methods , just use self
  • no attr_accessor , attr_reader and attr_writer for class variables in ruby
class Employee
  @@number_of_employees = 0

  def initialize
    @@number_of_employees += 1
    puts "Employee number #{@@number_of_employees} created successfully"
  end

  def self.number_of_employees=(number)
    @@number_of_employees = number
  end

  def self.number_of_employees
    @@number_of_employees
  end
end

puts Employee.number_of_employees   # => 0

Employee.new # Employee number 1 created successfully
Employee.new #Employee number 2 created successfully

puts Employee.number_of_employees # => 2

puts Employee.number_of_employees = 0

puts Employee.number_of_employees # => 0
Enter fullscreen mode Exit fullscreen mode

NOTE: you can define class level instance method using singleton class and class instance variables (Advanced you can read about it)


Understanding self

self which refers to the currently executing object, and changes depending on the scope it is used in
To make things clear, from within a class :

1- self, inside of an instance method, references the instance (object) that called the method - the calling object. Therefore, self.name= is the same as employee1.name=, in our example.

2- self, outside of an instance method, references the class and can be used to define class methods. Therefore if we define a class method, def self.number_of_employees=(n) is the same as def Employee.number_of_employees=(n).

class Employee
  puts "self is #{self}"

  def say
    puts "self is #{self}"
    puts "self class is #{self.class}"
  end
end
# self is Employee
Enter fullscreen mode Exit fullscreen mode

but when we call the instance method

employee = Employee.new
employee.say
# self is #<Employee:0x000055d5e3a00b90>
# self class is Employee
Enter fullscreen mode Exit fullscreen mode

use self.name VS name VS @name inside class:

1- self.name use self to force Ruby to work with instance methods like getter and setter
2- name in instance methods will look for local variables first then instance methods
3- @name will access the instance variable directly

  • in calling setter method you should use self to tell Ruby that you want to use the setter method of instance variable otherwise it will consider that you will set a local variable

  • the general rule from the Ruby style guide is to "Avoid self where not required." like inside an instance method to call getter, but it is good if we want to differentiate between instance variables and local variables with same names

class Employee
  attr_accessor :name, :age

  def initialize(name, age)
    @name = name
    @age = age
  end

  def set_local_age(value)
    # age here is local variable
    age = value
    # will use self to differentiate between age local variable and age instance variable
    puts "Local variable age: #{age}"
    puts "instance variable age: #{self.age}"
  end

  def set_instance_age(value)
    # we need to set the instance variable age, so use self
    self.age = value
    # for getter it is fine now as we did not have age local variable, so age = self.age =  getter method
    puts "instance variable age: #{age}"
    puts "instance variable age: #{self.age}"
  end

  def info
  # no need to use self
    self.age
  end
end

employee = Employee.new("Ali", 20)
puts employee.age # 20
puts employee.set_local_age(30)
# Local variable age: 30
# instance variable age: 20

puts employee.age # 20

puts employee.set_instance_age(30)
# instance variable age: 30
# instance variable age: 30

puts employee.age # 30

puts employee.info # 30
Enter fullscreen mode Exit fullscreen mode

Method Access Control

  • public : can be called by anyone as there is no access control (default)
  • private : only accessible from inside the class in which it was defined (or one of its subclasses).
  • protected : are similar to private methods in that they cannot be invoked outside the class. The main difference between them is that protected methods can also be called from within other instances of the has the same method (from same class or subclass) , while private methods do not

private and protected instance methods

  • you can declare them by use public or private or protected above group of methods , as a line divider so the access to methods will be controlled by the nearest access control keyword above it

  • it is better to make code readable by use each access control only once in class and put all method under it, so you can have at most 3 groups. you can list all the public first and then the others

class Employee
  def initialize(age)
    @age = age
  end

  def work
    do_something
  end

  def older?(other)
    age > other.age
  end

  def other_do_something(other)
    other.do_something
  end

  private

  def do_something
    puts 'I am working'
  end

  protected

  attr_reader :age
end

ali = Employee.new(64)
ahmed = Employee.new(42)

ali.work # I am working

ali.do_something # => NoMethodError (private method `do_something' called for #<Employee:0x000055d9855bee10 @age=64>)
ali.other_do_something(ahmed) # => NoMethodError (private method `do_something' called for #<Employee:0x000056276856cad0 @age=42>)

ali.older?(ahmed)  # => true
ahmed.older?(ali)  # => false
ali.age # => NoMethodError (protected method `age' called for #<Employee:0x000055d9855bee10 @age=64>)
Enter fullscreen mode Exit fullscreen mode
  • Alternatively, you can set access levels of named methods by listing them as arguments to the access control functions.
class Employee
  attr_reader :age

  def initialize(age)
    @age = age
  end

  def work
    do_something
  end

  def older?(other)
    age > other.age
  end

  def do_something
    puts 'I am working'
  end

  def other_do_something(other)
    other.do_something
  end

  protected :age
  private   :do_something
end
ali = Employee.new(64)
ahmed = Employee.new(42)

ali.work # I am working

ali.do_something # => NoMethodError (private method `do_something' called for #<Employee:0x0000561088ca9bf0 @age=64>)
ali.other_do_something(ahmed) # => NoMethodError (private method `do_something' called for #<Employee:0x0000561088ca9bf0 @age=42>)

ali.older?(ahmed)  # => true
ahmed.older?(ali)  # => false
ali.age # => NoMethodError (protected method `age' called for #<Employee:0x0000561088ca9bf0 @age=64>)
Enter fullscreen mode Exit fullscreen mode
  • or you can use the access control before the method definition (inlined)
class Employee
  private def private_method
    do_something
  end
end

Employee.new.private_method # NoMethodError (private method `private_method' called for #<Employee:0x0000556fb1f57b90>)
Enter fullscreen mode Exit fullscreen mode

private and protected class methods

  • The private and protected macros only affect instance methods of the current scope, not class methods.
  • for private class methods you can use private_class_method, we did not have one for protected as it is not often used
  • you can also use define private and protected class methods by open singleton class
class Employee
  def instance_method
    self.class.private_method
  end

  def self.public_method
    private_method
  end

  # will declare method access control after
  def self.private_method
    puts "Hi I am private"
  end

  private_class_method :private_method

  # inline
  private_class_method def self.private_method2
    puts "Hi I am private too"
  end
end

Employee.new.instance_method # NoMethodError (private method `private_method' called for Employee:Class)

Employee.public_method # Hi I am private

Employee.private_method # NoMethodError (private method `private_method' called for Employee:Class)

Employee.private_method2 # NoMethodError (private method `private_method2' called for Employee:Class)
Enter fullscreen mode Exit fullscreen mode

References and more information:

Top comments (0)