Modules in Ruby: Part I
In this article we’re going to explore the following topics:
- define a module
- module as namespace
- composition with the mixin facility
Introduction
A module is a collection of methods, variables, and constants stored in a container.
It’s similar to a class but it cannot be instantiated.
Define a Module
In Ruby, the module
keyword allows you to define a new module
Here the module
keyword starts the definition of a new module.
This module is named Greeting
.
Within this module, we define a hello
method.
Then we end the Greeting
definition by using the end
keyword.
Ok, cool... But what do I do with this now ?!
Modules have 2 main purposes:
- providing a namespace and preventing name clashes
- using the mixin facility for composition
Module as namespace
Namespacing is a way of bundling logically related objects together.
Modules serve as a convenient tool for this. This allows classes or modules with conflicting names to co-exist while avoiding collisions.
A good example of namespacing is the Rails
module.
This module contains a bunch of classes and modules — For example, the Rails::Application
class
Here we can see that the Application
class is defined within the scope of the Rails
module.
Note that to access this class from outside of the module Rails
, we use the ::
syntax.
As the Application
class is a pretty common class name that we could encounter in another gem then — and in order to avoid clashing — the Application
class is encapsulated in the Rails
module.
This means that the Rails::Application
class will never clash with an Application
class or a Dummy::Application
class that can be defined anywhere else.
Composition with the mixin facility
Ruby doesn’t handle multiple inheritance.
So how to organize a class to keep it maintainable in the long term?
The answer is by applying the Composition over Inheritance principle.
The Composition principle is based on the fact that a class should contain a set of objects that provide the desired functionality.
So instead of passing the functionality using the inheritance chain, it’s preferable to compose a class of objects that are responsible for providing the desired functionality.
Actually, Ruby facilitates the use of composition by using the mixin facility.
Indeed, a module can be included in another module or class by using the include
, prepend
and extend
keywords.
So let’s detail how to use these keywords
Before to start this section, feel free to read my article about the Ruby Object Model if you’re unfamiliar with the ancestor chain in Ruby.
The include keyword
Here, the Commentable
module is added to the ancestor chain of the Post
class by using the include Commentable
statement.
So, when we call the Post.new.comment
method — and as the Post#comment
isn’t defined — then it’s the Commentable#comment
method that is called.
What happens if we have two included modules that define a method with the same name?
Here, the Logger
class includes the Gem1
and Gem2
modules.
Both of these modules define a log
method.
So, when we call the $logger.log
method then the Gem2#log
method is called.
This seems logical when we have a look at the ancestor chain of the Logger
class.
Indeed, the Gem2
module appears right after the Logger
class in the Logger.ancestors
array.
So why Gem2
appears before Gem1
in the Logger
’s ancestor chain?
To answer this question, let’s detail step-by-step what happens when the Gem1
and Gem2
are included.
1- Gem1
is included: at this moment the Logger
’s ancestor chain is equal to:
[Logger, Gem1, Object, Kernel, BasicObject]
2- Gem2
is included: at this moment the Logger
’s ancestor chain is equal to:
[Logger, Gem2, Gem1, Object, Kernel, BasicObject]
So, the include
keyword inserts the module passed as a parameter just after the including class in the ancestor chain.
That’s why included modules are always inserted from the last inclusion to the first one in the including class’ ancestor chain.
Note that all the methods in the module are shared as instance methods in the including class.
The prepend keyword
Here, the Commentable
module is added to the ancestor chain of the Post
class by using the prepend Commentable
statement.
After, we notice that when the Post.new.comment
method is called then it’s the Commentable#comment
method that is called — Even though the Post#comment
method is defined.
This is due to the call to the prepend
keyword.
In effect, if we look at the Post
ancestor chain then we notice that the Commentable
module appears before the Post class itself.
This is why the Commentable#comment
is called instead of the Post#comment
one.
Except for this little change, include
and prepend
work pretty similarly.
The extend keyword
Here, the Commentable
module is added to the ancestor chain of the singleton class ofPost
— represented by #<Class:Post>
in the singleton class
ancestor chain — by using the extend Commentable
statement.
To keep it simple: in our case, the singleton class is where class methods are defined for a given Ruby class.
The singleton class is a way more complex than this definition. This is why I’ll cover it in a dedicated article.
So when we call the Post.comment
class method then it’s the Commentable.comment
method that is called.
So the extend
keyword simply inserts a module right after the singleton class in the singleton class
ancestor chain.
In the part II we’re going to cover the
Module.new
method. So stay tuned ! ;-)
RubyCademy’s Newsletter
In the RubyCademy newsletter, I share what I’m learning about Ruby & Rails, including helpful tips, code insights, and exclusive Ruby Cards with detailed explanations.
If it sounds helpful, feel free to subscribe for free:
Thank you for taking the time to read this message!