Ruby OOP review session (Part 1)
Tags: ruby, oop, beginners, programming
Published: 2023-03-15 20:49:01

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

```ruby
'hi'.class # => String

1.class #=> Integer
```

---

## 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**

```ruby
class Employee
  # class code live here
end
```

---

## Create objects (instantiation)
- use `new` class method to create object

```ruby
employee = Employee.new
```

---

### `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

```ruby
class Employee
  def initialize
    puts "This object was initialized!"
  end
end

employee = Employee.new #=> This object was initialized!
```

---

## 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.

```ruby
class Employee
  def initialize(name)
    @name = name
  end
end

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

---

### Instance Methods

- define behavior of instance _(something instance can do)_
- methods that objects can call

```ruby
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"
```

---

### Accessor Methods

- in the above example we can not access instance variable `name` outside the class.

```ruby
employee1.name # => NoMethodError (undefined method `name' for #<Employee:0x0000561c18d842c8 @name="Sara">)
```

- `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.

```ruby
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
```

- 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

```ruby
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"
```

---

### 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

```ruby
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
```

- 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 

```ruby
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
```

---

### 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

```ruby
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)
```

---

### 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

```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
```

**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)`.

```ruby
class Employee
  puts "self is #{self}"

  def say
    puts "self is #{self}"
    puts "self class is #{self.class}"
  end
end
# self is Employee
```

but when we call the instance method

```ruby
employee = Employee.new
employee.say
# self is #<Employee:0x000055d5e3a00b90>
# self class is Employee
```

#### 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

```ruby
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
```

---

### 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

```ruby
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>)
```

- Alternatively, you can set access levels of named methods by listing them as arguments to the access control functions.

```ruby
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>)
```

- or you can use the access control before the method definition (inlined)
```ruby
class Employee
  private def private_method
    do_something
  end
end

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

#### 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

```ruby
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)
```

## References and more information:

- [launch school](https://launchschool.com/books/oo_ruby/read/the_object_model#whyobjectorientedprogramming)
- [Classes and Objects](https://ruby-doc.com/docs/ProgrammingRuby/html/classes.html)
- [Ruby setter return the passed value, rather than the final value of the instance variable?](https://stackoverflow.com/questions/65226532/why-does-a-ruby-setter-return-the-passed-value-rather-than-the-final-value-of-t)
- [private class method](https://stackoverflow.com/questions/4952980/how-to-create-a-private-class-method)
- [Why do Ruby setters need "self." qualification within the class?](https://stackoverflow.com/questions/44715/why-do-ruby-setters-need-self-qualification-within-the-class)
- [
  Explaining Ruby's Singleton Class (Eigenclass) to confused beginners ](https://dev.to/samuelfaure/explaining-ruby-s-singleton-class-eigenclass-to-confused-beginners-cep)