Ruby Object Oriented Programming 📦 - Refresher


What are Objects ?

Objects are what classes make.

How to create an object ?

Creating an object is easy, you create a class then you instantiate it to make an object,below is an example of an object:

class NormalCat
  def initialize(name)
    @name = name
  end
end

chips = NormalCat.new("Chips")
# This create a a cat 🐈 object named "chips"

What is OOP ?

OOP is a programming paradigm that manages the complexity of large software systems, this paradigm helps us avoid the debugging hell that happens just because we changed some code. tell me about neck pain he said😮‍💨.

OOP helps us by sectioning off a bunch of code to objects that can talk with each other instead of making everything in one mountain ⛰️

How does OOP make our life easier ?

By Encapsulation

The concepts of Encapsulation is that we combine variables and methods that work with the data into an object.

By other words, by using encapsulated object OOP makes our life easier.

Again, by storing variables and methods that work with some data in an object you encapsulate them, with this you get the power of hiding 😶‍🌫️ the code from other objects, this means nothing can manipulate your hidden code unless you want to, you can expose your object to other objects by using methods to interact with other objects. like setter and getters.

Inheritance

It's when when a class inherit the behaviors of another class aka the super-class

Polymorphism

Polymorphism is a core concept in OOP that allows objects of different classes to respond to the same method call in different ways.

class Animal
  def speak
    "Some sound"
  end
end

class Dog < Animal
  def speak  # Overrides Animal#speak
    "Woof!"
  end
end

class Cat < Animal
  def speak  # Overrides Animal#speak
    "Meow!"
  end
end

animals = [Dog.new, Cat.new]
animals.each { |animal| puts animal.speak }
# Output:
# Woof!
# Meow!

Modules

It's another way to inherit behavior but this it is different because we don't make objects out of module but rather just plug them into an object by using include

module Debuggable
  def debug_info
    puts "#{self.class} - Object ID: #{object_id}"
  end
end

class Product
  include Debuggable

  def initialize(name)
    @name = name
  end
end

class User
  include Debuggable

  def initialize(name)
    @name = name
  end
end

product = Product.new("Laptop")
user = User.new("Alice")

product.debug_info  # Output: Product - Object ID: 70307294386140
user.debug_info     # Output: User - Object ID: 70307294386120

More on classes and objects

initialize method

class NormalCat
  def initialize
    puts "This object was initialized!"
  end
end

chips = NormalCat.new("Chips")     # => "This object was initialized!"

It's a methods that gets initialized at object creation.
You may know it as the constructor.

Instance variables

class NormalCat
  def initialize(name)
    @name = name
  end
end

@name is an instance variable. It is a variable that exists as long as the object instance exists

Instance Methods

class NormalCat
  def initialize(name)
    @name = name
  end

  def speak
    "meow!"
  end
end
chips = NormalCat.new("Chips")
puts chips.speak # => "meow!"

Class methods

They are not instance methods, class methods are methods we can call directly on the class itself without having to instantiate any object

def self.something
  print "nothing"
end

Then we just call it on a class cat.something --> "nothing", class methods are best when it does not need to deal with the state of the data.

Class variables

just like class methods but as variables, you can create a class variable using two @@

Composition and Aggregation

We talked about making objects talk with each other but how ? there are some design principles used to establish relationships between classes.
Both composition and aggregation involve using instance variables to hold references to other objects, but they differ in terms of the lifecycle and ownership of the objects involved.

Composition

composition is a relationship where an object contain other objects as part of its state , they depend on each other. If you destroy the container it will also destroy it's other contained objects

It's easy to implement composition, you only have to use instance variables . Here is an example :

class Engine
  def start
    puts "Engine starting..."
  end
end

class Car
  def initialize
    @engine = Engine.new  # Engine instance is created when Car is created
  end

  def start
    @engine.start
  end
end

my_car = Car.new
my_car.start  # Engine is an integral part of Car

In this example Car object has initialized an instance variables that contains the Engine object, this gives the Car an Engine with all of it's powers/properties .
Remember, objects live and die together.

Did you know ? Godot uses Composition for it nodes for building games !

Aggregation

Aggregation is a less restrictive design principle because contained object do not live and die with container object.
This is possible because the container only references the contained objects. Here is an example :

class Passenger
end

class Car
  def initialize(passengers) 
    @passengers = passengers  # Passengers are given to the Car at creation
  end
end

# Passengers can exist without Car
passengers = [Passenger.new, Passenger.new]
my_car = Car.new(passengers)

In this example, Car.new gets an array of passengers at creation, those passengers are independent from the car, this means that we can pass the passengers in the Car at as long as the Car object is alive.

Container Object relations to instance variables

The relationships between a container class instance and its composed and aggregated objects is implemented with instance variables. These instance variables hold references to other objects, enabling the container class to access and interact with the contained object's methods and properties. The main difference lies in the ownership and the lifecycle of the objects referenced by these instance variables:

  • Composition: The container owns the contained objects, and their lifecycles are tightly linked.

- Aggregation: The container does not own the contained objects; they can exist independently.

These concepts are fundamental in designing systems that are modular, where changes to one part of the system do not unduly affect others.

speak is an instance methods

Accessor Methods

lets say that we have this cat , how can we know the name of the cat?

class NormalCat
  def initialize(name)
    @name = name
  end

  def speak
    "meow!"
  end
end
orange_cat = NormalCat.new("chonk")
puts chips.speak # => "meow!"

let us try puts orange_cat.name, oh no we got a :

undefined method `name' for an instance of NormalCat (NoMethodError)

If we want to access the orange_cat's name, which is stored in the @name instance variable, we have to create a method that will return the name. We can call it get_name, and its only job is to return the value in the @name instance variable.

class NormalCat
  def initialize(name)
    @name = name
  end

  def speak
    "meow!"
  end
  def get_name
    @name # instance variable
  end
end
orange_cat = NormalCat.new("chonk")
puts orange_cat.speak # => "meow!"
puts orange_cat.get_name # => "chonk"

okay but what if we want to change orange_cat's name ? then we make a setter method !

class NormalCat
  def initialize(name)
    @name = name
  end

  def speak
    "meow!"
  end
  def get_name
    @name # instance variable
  end
  def set_name=(name)
    @name = name
  end
end
orange_cat = NormalCat.new("chonk")
puts orange_cat.speak # => "meow!"
puts orange_cat.get_name # => "chonk"
orange_cat.set_name = "kurt"
puts orange_cat.get_name

attr_accessor

Ruby has a built-in way to automatically create these getter and setter methods for us, using the attr_accessor method.

The attr_accessor method takes a symbol as an argument, which it uses to create the method name for the getter and setter
methods.

**attr_reader**
It's a getter only
**attr_writer**
It's a setter only.

All of the attr_* methods take Symbol objects as arguments; if there are more states you're tracking, you can use this syntax: attr_accessor :name, :height, :weight
try to use self with attr_accessor has created to the job of instance methods to let Ruby know that we are calling the methods and that we are not defining a new local variables !

Note , try not to use a instance variables outside of the initialize constructor , use an instance methods that can use from attr_accessor aka getters and setters and store them in a methods , it's better for maintaining clean code.