DJ Kitten inherited this keyboard. So he wanna compose his new hit with it..

Modules in Ruby: Part I

Tech - RubyCademy
RubyCademy

--

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 ! ;-)

Ruby Mastery

We’re currently finalizing our first online course: Ruby Mastery.

Join the list for an exclusive release alert! 🔔

🔗 Ruby Mastery by RubyCademy

Also, you can follow us on x.com as we’re very active on this platform. Indeed, we post elaborate code examples every day.

💚

--

--