According to Wikipedia, object-oriented programming (OOP) is a programming paradigm based on the concept of "objects", which can contain data and code: data in the form of fields (often known as attributes or properties) and code in the form of procedures (often known as methods).
Ruby is a pure object-oriented language, which means that in the Ruby language, everything is an object. These objects, regardless of whether they are strings, numbers, classes, modules, etc., operate in a system called The Object Model.
Ruby offers a method called the object_id
, which is available to all objects. This identifier returns an integer and is never the same for any two objects. Let's get into irb to get our hands dirty; you can do this by typing irb
in your terminal.
As seen above, strings, integers, arrays, classes, and even methods are all objects, as they possess an object ID.
In this article, we'll cover the following concepts in detail:
- Classes and instances
- Inheritance
- Public, private, and protected methods
- Mixins
- Modules
- Object hierarchy
Classes and Instances
In Ruby, classes are where the attributes and attitudes(actions) of an object are defined. If an object is round (an attribute) and should be able to speak (an action), we can tell from the class it belongs to because these attributes and actions are defined as something called methods in that class.
An object belonging to a class is called an instance of that class and is created (instantiated) using .new
. Let's start by creating a class called Human
. I suppose we're all humans, so this should be fun.
class Human
def initialize(name)
@name = name
end
end
The initialize method identifies the requirements for a new instance of a class to be created. In the case above, we can see that to create a new human, a name is required. Hence, a new instance of human can be created using the command Human.new(name)
, where the name is whatever you choose. In our case, let's use 'Henry'. To test this in our irb environment, all we need to do is load the file using the command load './path_to_filename'
and re-use the command for file re-execution whenever changes are made. In my case, it is load './Human.rb'
because irb is opened in the folder containing the said file. To run the code without irb, we need to add a puts
statement before every command so that the results will be visible.
When we try to create a new human without a name, we get an argument error because a name is required. However, when we do it correctly, we see that the human named Henry is created and belongs to the class Human
. Henry is therefore an instance of the class Human
.
The @name
variable is called an instance variable because of the @
symbol it begins with, which means that this variable can be referenced by any other method within the class, as long as the class instance in question exists. Here, we have created it and set it equal to the name with which the object was initialized.
Let's proceed to defining the traits and actions of any object belonging to this class. Since the created objects are called instances of the class, the methods that define their behavior are called instance methods. Humans have a name and certain body parts and can perform certain actions, so let's define them.
def name
@name
end
def no_of_legs
2
end
def no_of_hands
2
end
def speak
'blablabla'
end
We have added methods that retrieve the name of the created human, define the number of legs and hands a human has, and give the human the ability to speak. We can call these methods on the class instance using instance.method_name
, as shown below.
What if we change our minds and decide that we want to change the name of our class instance Henry. Ruby has a built-in method with which we can do this, but it can also be done manually. Manually, we can change our name method from being just a getter method that gets the name to a setter method that also sets it to a value if one is provided.
def name=(new_name)
@name = new_name
end
Using Ruby’s built-in attr_accessor
method, we can discard our name method and replace it with the line attr_accessor :name
:
class Human
attr_accessor :name
def initialize(name)
@name = name
end
# rest of the code
end
Regardless of the chosen method, at the end of the day, this is what is obtainable.
All the methods created thus far are called instance methods
because they can be called on any instance of the class but not the class itself. There is also something called a class method
, which can be called on the class itself and not on its instances. Class methods are named by prefixing the method name with self.
. Here’s an example:
# within the Human class
def self.introduction
"I am a human, not an alien!"
end
As we can see above, the class method is only available to the class itself and not its instances. In addition, just as instance variables exist, we have class variables, which are prefixed with two @
symbols. An example is shown below.
class Human
attr_accessor :name
@@no_cars_bought = 0 #class variable
def initialize(name)
@name = name
end
def self.no_cars_bought
@@no_cars_bought
end
def buy_car
@@no_of_cars_bought += 1
"#{@name} just purchased a car"
end
end
In this example, we added a buy_car
method to enable every human to purchase a car. We also created a class variable called @@no_of_cars_bought
that increases by 1 each time a car is bought. Lastly, we created a class method called no_cars_bought
that fetches the number of cars that have been bought. Let's see how this works:
All the methods defined so far are known as public methods because they are accessible outside the class. We can also define methods called private methods, which can only be accessed within a class. Let's try an example.
# at the bottom of the Human class
def say_account_number
"My account number is #{account_number}"
end
private
def account_number
"1234567890"
end
This yields the following:
When we call henry.account_number
, we get a "NoMethodError" because account_number
is a private method that can only be accessed from within the class. When accessed via the say_account_number
method as we did, there are no errors, as this method exists within the same class as the private method. It is important to note that every instance method after the private
keyword becomes a private method; hence, private methods should be defined at the bottom of the class after all the public methods.
Have you ever heard of protected methods? Yeah, right! These exist too, but we'll talk about them after we understand the concept of inheritance.
Inheritance
Now that we know about classes and instances, let's proceed to talking about inheritance. To properly understand the concept of inheritance, let's create a new class called Mammal
since humans are mammals. I recall a certain phrase in science class: "All humans are mammals, but not all mammals are humans". I also recall that some of the characteristics of mammals include the presence of hair or fur and a complex brain. Let's put this information in the Mammal
class.
class Mammal
def has_hair?
"Most certainly, Yes"
end
def has_complex_brain?
"Well, as a mammal, what do you expect?"
end
end
Do you recall my science class phrase? If so, it is important that we create a relationship that validates this statement. So, what do we do? We permit the human class to inherit the properties of the Mammal
class by adding < Mammal
to its class definition.
class Human < Mammal
# all other code
end
A class that is inheriting the properties of another is called a subclass, and the class it inherits from is called a superclass. In our case, Human
is the subclass and Mammal
is the superclass. It is important to note at this point that if you are defining all the classes in one file like we are doing right now, the Mammal
class definition should come before the Human
class in your file, as we don't want to refer to a variable before it is defined. Let's see what additional features the human has now.
As shown above, the human now has access to all the attributes defined in the Mammal
class. If we remove the inheritance (i.e., we remove < Mammal
from that line of code) and run the command henry.class.superclass
, we get "Object" as our response. This tells us that every class when not directly inheriting from another class has its superclass as "Object", which further strengthens the fact that in Ruby, even classes are objects.
Now that we know about what superclasses are, this is the perfect time to talk about the keyword super. Ruby provides this keyword to enable the reuse and modification of methods that already exist on a superclass. In our Mammal
superclass, recall that we have a method called has_hair?
; what if we want to add some additional information specific to humans whenever that method is called? This is where the use of the super keyword comes in. In our Human
class, we define a method with the same name, has_hair?
.
def has_hair?
super + ", but humans can be bald at times"
end
When the super keyword is called, Ruby looks for that method name in the superclass and returns its result. In the above method, we have added some extra information to the result produced by the superclass' has_hair?
method.
Ruby only supports single class inheritance, which means that you can only inherit class properties from one class. I'm sure you're wondering what would happen if you wanted to add multiple properties not specific to a particular class to your class. Ruby also makes a provision for this in the form of mixins, but before we talk about them, let's talk about protected methods.
Protected Methods
Protected methods function like private methods in that they are available to be called within a class and its subclasses. Let's create a protected method within the Mammal
class.
#at the bottom of the Mammal class
def body_status
body
end
protected
def body
"This body is protected"
end
As shown above, we cannot access the body
method because it is protected. However, we can access it from the body_status
method, which is another method inside the Mammal
class. We can also access protected methods from subclasses of the class where it is defined. Let's try this within the Human
class.
# within the Human class
def check_body_status
"Just a human checking that " + body.downcase
end
As shown above, regardless of whether the body method is defined within the Human
class and protected, the Human
class can access it because it is a subclass of the Mammal
class. This subclass access is also possible with private methods; however, this leads to the question of what difference exists between these methods.
Protected methods can be called using explicit receivers, as in receiver.protected_method
, as long as the receiver in question is the keyword self
or in the same class as self
. However, private methods can only be called using explicit receivers when the explicit receiver is self
.
Let's edit our code by replacing body.downcase
with self.body.downcase
in the check_body_status
method and changing our private method call to also include self
.
def check_body_status
"Just a human checking that " + self.body.downcase
# body method is protected
end
def say_account_number
"My account number is #{self.account_number}"
# account_number method is private
end
P.S.: Before Ruby 2.7, when calling a private method to get some information, using self
was impossible.
Let's go ahead and replace the word self
with an object in the same class as self
. In our case, self
is Henry
, who is calling the methods, and Henry
is an instance of the Human
class, which inherits from the Mammal
class.
def check_body_status
non_self = Human.new('NotSelf') #same class as self
puts "Just #{non_self.name} checking that " + non_self.body.downcase
# Mammal.new is also in the same class as self
"Just a human checking that " + Mammal.new.body.downcase
end
def say_account_number
non_self = Human.new('NotSelf')
"My account number is #{non_self.account_number}"
end
As shown above, it is possible to replace self
in protected methods with an object of the same class as self
, but this is impossible with private methods.
Mixins
Mixins are a set of code defined in a module, which, when included in or extended to any class, provides additional capabilities to that class. Several modules can be added to a class, as there is no limit to this, unlike what is obtainable in class inheritance. In light of this information, let's create a module called Movement
to add movement abilities to the human class.
module Movement
def hop
"I can hop"
end
def swim
"Ermmmm, I most likely can if I choose"
end
end
The next step would be to include this module in our Human
class. This is done by adding the phrase include <module_name>
to the class in question. In our case, this would entail adding include Movement
to the human class, as shown below.
class Human < Mammal
include Movement
# rest of the code
end
Let's test this code:
As shown above, the methods mixed into the class via the module are only available to the instances of the class and not the class itself. What if we want to make the methods in the module available to the class and not the instance? To do this, we replace the "include" term with "extend", as in extend Movement
.
The .extend
term can also be used on class instances but in a very different manner. Let's create another module called Parent
.
module Parent
def has_kids?
"most definitely"
end
end
As shown above, using .extend(Parent)
on the dad variable makes the has_kids?
method available to it. This is especially useful when we don't want the module's methods mixed into every class instance. Thus, extend
can be used only on the particular instance in which we're interested.
Modules
Modules are housing for classes, methods, constants, and even other modules. Aside from being used as mixins, as seen earlier, modules have other uses. They are a great way to organize your code to prevent names from clashing, as they offer a benefit called namespacing. In Ruby, every class is a module, but no module is a class, as modules can neither be instantiated nor inherited from. To understand namespacing, let's create a module containing a constant, a class, a method, and another module.
module Male
AGE = "Above 0"
class Adult
def initialize
puts "I am an adult male"
end
end
def self.hungry
puts "There's no such thing as male food, I just eat."
end
module Grown
def self.age
puts "18 and above"
end
end
end
You might have noticed that the methods defined and called within the modules above are prefixed with self.
. This is because modules cannot be instantiated, and any method defined within them without the self.
prefix will only be available as a mixin, as we saw earlier when we discussed mixins
. To call a method on a module itself, we must identify this on the method name via the use of self.method_name
. Recall that we also used the self
keyword in class methods; the same principle applies here.
As shown above, to reach the classes, modules, or constants defined within a module, we use module_name::target_name
. To properly understand the namespacing concept, we'll create another module containing a class named Adult
, and then we'll see how both classes are differentiated.
module Female
class Adult
def initialize
puts "I am an adult female"
end
end
def self.hungry
puts "Maybe there's such a thing as female food, I'm not sure. I just need to eat"
end
end
As shown above, we have two classes bearing the name "Adult". By wrapping them within their own modules, we're able to use these names without any confusion about the exact adult class being called each time. Furthermore, our code is more readable and more organized, and we implement the separation of concerns design principle, as we separate different code blocks into several categories based on their functions.
Object Hierarchy
It's amazing to understand classes, instances, modules, and the concept of inheritance, but knowledge in this area is incomplete without understanding the object hierarchy in Ruby. This refers to the order in which a method is searched for in Ruby. A few methods exist to help us understand this hierarchy; one of them is the ancestors
method. This method is not available to class instances but can be called on the classes themselves and their ancestors. An example is shown below.
From the result of Human.ancestors
, we see that this method returns the class in question, its directly included modules, its parent class, and the parent's class' directly included modules; this loop continues until it gets to Basic Object
, which is the root of all objects.
Another available method to get more information about a class is the method included_modules
.
As shown above, the human class has two modules included in it: the one which we directly included using include Movement
and that which is included in the Object class. This means that each class inherits class properties from its ancestor classes, and each module included in them gets included in it.
Based on this information, we'll carry out a simple exercise to confirm the method lookup path in Ruby and what classes have priorities over others in this path. We'll define methods with the same name but different output strings and place them in the Human
class, Movement
module, and Mammal
class.
# in the mammal class
def find_path
"Found me in the Mammal class path"
end
# in the Movement module
def find_path
"Found me in the Movement module path"
end
# in the Human class
def find_path
"Found me in the Human class path"
end
Now let's carry out this exercise.
As shown above, the order in which the ancestors are laid out when we call .ancestors
on a class is the path followed when looking for a method called on an instance of that class. For the Human
class instance we created, Ruby first searches for the method in the class itself; if not found, it proceeds to any directly included modules; if not found, it proceeds to the superclass and the cycle continues until it gets to the BasicObject
. If the method is still not found there, a "NoMethodError" is returned. By using the .ancestors
method, we're can identify the lookup path for an object and where its methods emanate from at any point in time.
Ruby also offers a method called methods
, by which we can identify all the methods available to any object. We can even carry out subtractions to show which come from its parent and which are peculiar to it. An example is shown below.
As shown above, the variable Henry has a lot of methods available to it. However, when we subtract them from those available to the Object class, we find that we're left with only those we specifically defined in our file, and every other method is inherited. This is same for every object, its class, and any of its ancestors. Feel free to get your hands dirty by trying out several combinations involving Human.methods, Mammal.methods, Module.methods, and every other class or module defined earlier; you will find that this gives you a stronger grasp of the Ruby object model.