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.