DEV Community

loading...
Cover image for My Journey into Ruby: Modules

My Journey into Ruby: Modules

d4vsanchez profile image David Sánchez Updated on ・6 min read

Let's talk about modules in Ruby, how they differ to classes, why are they useful and how does the Ruby method lookup works.

Modules in Ruby are similar to classes, they hold multiple methods. However, the modules cannot be instantiated as classes do, the modules do not have the method new.

We can think of modules as mix-ins, they allow you to inject some code into a class. The mix-in exports the desired functionality to a child class, without creating an "is a" relationship. In Ruby, the main difference between inheriting from a class and mixing a module is that you can mix in more than one module.

The Well-Grounded Rubyist book has a really good example about when a module may be needed.

💡 When you're designing a program and you identify a behavior or set of behaviors that may be exhibited by more than one kind of entity or object, you've found a good candidate for a module.

Let's review the basics of module creation. We will use "stack-likeness" as our main example. We want to create a module that introduces stack-like behavior to any class.

ℹ️ A stack is a one dimensional data structure that follows a particular order for the operations to be performed on it: LIFO (Last In First Out), the last element to be added into the stack will be the first element to be queried on the stack.

Module Creation

Writing a module follows almost the same syntax that we use to create classes, the only difference is that we need to start the definition using the module keyword instead of the class keyword:

module Stacklike
    def stack
        @items ||= []
    end

    def push(obj)
        items.push(obj)
    end

    def pop
        items.pop
    end
end
Enter fullscreen mode Exit fullscreen mode

What we did here is pretty simple. We're creating an instance variable called @stack that is an array representing our stack (1D data structure we talked about above) and we then abstract the push and pop behaviors of the stack into the push and pop methods of the module.

Mixing the Module Into a Class

In order to mix the module into a class we are going to make use of one of three different keywords: include, prepend and extend.

⚠️ For this example, I'm going to show how to use the include keyword to add Stack-likeness into our class, to understand how the other keywords work we'll need to go through how the method lookup works in Ruby and we'll do that later in this article.

class Stack
    include Stacklike
end
Enter fullscreen mode Exit fullscreen mode

That's it. That's all that is needed in order to mix our Stacklike module into our Stack class, and now it can behave like a stack.

stack = Stack.new
stack.push("First item")
stack.push("Second item")
stack.push("Third item")
puts "Objects currently in the stack:"
puts stack.items
last_item = stack.pop
puts "Removed from the stack:"
puts last_item
puts "Objects currently in the stack:"
puts stack.items
Enter fullscreen mode Exit fullscreen mode

If we execute this code, we'll see that the stack behavior is currently being applied to our Stack class and the Stack-likeness behavior can be introduced to any other class that we want in our codebase, it is easily reusable.

💡 Rubyists often use adjectives for module names in order to reinforce the notion that the module defines a behavior.

Method Lookup Path

The method lookup path is a mechanism in which Ruby starts "bubbling up" in the "object chain" in order to find what object or module is the one that contains the method that we're currently calling. This is what's being used when we call object.pop or object.items in our past example, those methods are not explicitly defined in Stack, but they're found in the method lookup path of the Class by including the Stacklike module.

Consider the following code:

module M
    def x
        puts "Hey! I'm the module M"
    end
end

class B
    def x
        puts "Hey! I'm the class B"
    end
end

class A < B
    includes M
end
Enter fullscreen mode Exit fullscreen mode

We are creating a class (A) that inherits from another class (B) and also mixes the behavior that's provided by a module (M). Now, we can instantiate the class A and call the method X, by following the Method Lookup Path, we'll get "Hey I'm the module M" as the result.

a = A.new
a.x # Hey I'm the module M
Enter fullscreen mode Exit fullscreen mode

Include Method Lookup Path

The Class A object will try to find a method to execute based on the message that has been received (x). If Ruby has looked up all the way in the object chain until it reaches out Kernel or BasicObject and still hasn't found it, then it won't be found.

ℹ️ The point of BasicObject is to have as few instance methods as possible, so it doesn't provide much functionality. Most of the functionality or Ruby's fundamental methods is actually found in the Kernel module, that are included in all objects that descend from Object.

By looking at the diagram above, you'll note that the way the method x is found in the example is by following the path: [A, M, B, Object, Kernel, BasicObject] and as the method x is found in M, it will use that instead of using the one found in B. You can use the ancestors method to find this path in any object, it is provided by the Kernel module.

ℹ️ If two modules are included in the same class and they both contain a method with the same name, they're going to be searched in reverse order of inclusion. The last mixed-in module is searched first.

Prepend and Extend Keywords

When mixing-in the first module into the class, I told that the prepend and extend keywords could be used to that as well. Let's see the differences between them.

Prepend

It works almost the same as include, the difference is that when you prepend a module to the class, the object will look for the method in the module first, before looking for it in the class.

Consider this change in the code above:

class A < B
    prepend M

    def x
        puts "Hey! I'm the class A"
    end
end
Enter fullscreen mode Exit fullscreen mode

The method lookup path will change to [M, A, B, Object, Kernel, BasicObject].

Prepend Method Lookup Path

Extend

Both include and prepend will make the methods of the modules available as instance methods of the class. extend works a bit different by making module's methods available as class methods instead. Extending an object doesn't add the module into the ancestor chain.

Super Method

There's this keyword called super that we can use inside the body of a method definition. What this keyword does is that it jumps to the next-highest definition of the current method in the method lookup path.

Consider the following change in the example code:

module M
    def x
        puts "Hey! I'm the module"
        puts "But I'm going to call the next higher-up method..."
        super
        puts "Back in the module"
    end
end
Enter fullscreen mode Exit fullscreen mode

Calling the method x will result in something like:

a = A.new
a.x

# Hey I'm the module
# But I'm going to call the next higher-up method
# Hey I'm the Class B
# Back in the module
Enter fullscreen mode Exit fullscreen mode

Because the super keyword is going to call the method x that is found in the Class B which is the next object that is available in the method lookup path in this example.

It's also important to note that the super keyword handles arguments in a different way as methods would do:

  • When called with no argument list, it will automatically forward the arguments that were passed to the method from which it's called.
  • When called with an empty argument list (super()), it sends no arguments to the higher up method, even if there were arguments passed to the current method.
  • When called with specific arguments (super(1, 2, 3)), it sends exactly those arguments to the higher up method.

When to Use Mix-Ins vs Inheritance

Having both inheritance and modules means that you have a lot of choice, but having a lot of choice also means that you also must be very careful about the considerations both this approaches introduce.

  • As noted at the beginning of this post, modules don't have instances. Entities or things are better modeled using classes, while behaviors or properties are better encapsulated using modules.
  • Classes can have a single superclass, but can mix in as many modules as needed.

You may like to break everything into separate modules, because you think something that you write for one entity may be useful in another entities in the future. But the overmodularization also exists. You've got the tools, and is up to you to consider how to balance them.


Hope you've liked this post, I learned a lot while studying modules and I tried to summarize it as much and as easy I could in this post.

If you've got any feedback or question about this post, please add it as a comment. I'm sure we all could learn about this.

Thanks for your time ❤️👋

Discussion (2)

Collapse
topofocus profile image
Hartmut B.

Nice introduction.

Modules can be used to create real singletons in ruby.
from github.com/ib-ruby/ib-symbols/blob... :

module  IB
  module Symbols
    def self.allocate_m name 
      m= Module.new do
            extend Symbols
            (..)
            def self.yml_file    (...)  end
        end   # Module new             
        (..)
      name =  name.to_s.camelize.to_sym
      Symbols.const_set  name, m    # generate the module-const
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Then
IB::Symbols.allocate_m :c
creates the module IB::Symbols::Cand the defined methods.

No class is involved in the hole approach .

IB::Symbols::C.yml_file is a real singleton.

Collapse
diegoorejuela profile image
DiegoOrejuela

Good article. What is the difference with 'Concerns' (available on RoR)? 🤔

Forem Open with the Forem app