DEV Community

Honeybadger Staff for Honeybadger

Posted on • Originally published at honeybadger.io

Understanding the Ruby Object Model In Depth

This article was originally written by Abiodun Olowode on the Honeybadger Developer Blog.

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.

Object Ids for Objects

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
Enter fullscreen mode Exit fullscreen mode

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.

Instantiating the human class

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
Enter fullscreen mode Exit fullscreen mode

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.

Call methods on Instance

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Regardless of the chosen method, at the end of the day, this is what is obtainable.

Change instance name

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
Enter fullscreen mode Exit fullscreen mode

Class methods

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
Enter fullscreen mode Exit fullscreen mode

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:

Class Variables

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
Enter fullscreen mode Exit fullscreen mode

This yields the following:

Private methods

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Inheritance

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.

Object Class

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
Enter fullscreen mode Exit fullscreen mode

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.

Super keyword

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
Enter fullscreen mode Exit fullscreen mode

Protected Method

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
Enter fullscreen mode Exit fullscreen mode

Protected Methods 2

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
Enter fullscreen mode Exit fullscreen mode

Self usage

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
Enter fullscreen mode Exit fullscreen mode

Protected methods use not-self calls

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Let's test this code:

Module added using include

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.

Module added using extend

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
Enter fullscreen mode Exit fullscreen mode

Extend for class instances

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
Enter fullscreen mode Exit fullscreen mode

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.

Modules for namespacing

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
Enter fullscreen mode Exit fullscreen mode

Module for namespacing

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.

checking ancestors

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.

Included_modules method

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
Enter fullscreen mode Exit fullscreen mode

Now let's carry out this exercise.

Method lookup path

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.

Using .methods

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.

Discussion (0)