My Journey into Ruby: Modules
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
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.
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
module Stacklike def stack @items ||=  end def push(obj) items.push(obj) end def pop items.pop end end
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:
⚠️ For this example, I'm going to show how to use the
includekeyword 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
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
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.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
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
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
extend keywords could be used to that as well. Let's see the differences between them.
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
The method lookup path will change to
[M, A, B, Object, Kernel, BasicObject].
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.
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
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
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.