BrainsToBytes

Ruby modules in-depth

Modules are incredibly flexible and powerful constructs that help you organize your code in a more... modular way. They let you package related functionality in a cohesive unit you can use to extend the behavior of objects and classes.

This article includes code samples (the link is in the 'What to do next' section at the end of the article)

Using modules to bundle together related behavior

Ruby gives you two options to bundle behavior into units: classes and modules. Both are very similar in many ways, but have two important differences:

  • You can instantiate a class, but not a module.
  • In OO ruby, a class is usually used to represent an entity, whereas modules represent characteristics or behavior. You write classes for things like Planes, People and Coral. Modules are for properties like Magical, Rational or Approvable.

Both classes and modules let you package together related behavior. Classes let you create several objects from them, each one with individual instance variables to hold its state. If you find yourself writing classes without instance variables or methods, you might want to use a module instead.

Consider the case where you need to write a simple linear motion calculator. Because the operations are stateless and simple, you probably won't need to create instances. Let's see what a module with that functionality looks like.

module ULMCalculator
    # all speeds in m/s
    def self.calculate_speed(distance_in_meters:, time_in_seconds:)
        raise ArgumentError, "We can't calculate for time = 0" if time_in_seconds == 0 

        speed = distance_in_meters / time_in_seconds
        puts "An object that moves #{distance_in_meters}m in #{time_in_seconds}s has a speed of #{speed}m/s"
        return speed
    end

    def self.calculate_distance (speed:, time_in_seconds:)
        distance_in_meters = speed * time_in_seconds
        puts "An object with a speed of #{speed}m/s moving for #{time_in_seconds}s travels a distance of #{distance_in_meters}m"
        return distance_in_meters
    end

    def self.calculate_time(distance_in_meters:, speed:)
        raise ArgumentError, "We can't calculate for speed = 0" if speed == 0    
                           
        time_in_seconds = distance_in_meters / speed
        puts "It takes an object #{time_in_seconds}s to move #{distance_in_meters}m if it moves at #{speed}m/s"
        return time_in_seconds
    end
end

This is an example of how we could use the module to compute some simple cases:

require_relative 'uniform_linar_motion_calculator'

ULMCalculator.calculate_speed(distance_in_meters: 100, 
                              time_in_seconds: 5)
                              
ULMCalculator.calculate_distance(speed: 100, 
                                 time_in_seconds: 10)

ULMCalculator.calculate_time(distance_in_meters: 100, 
                             speed: 25)

Running this code prints the following:

An object that moves 100m in 5s has a speed of 20m/s
An object with a speed of 100m/s moving for 10s travels a distance of 1000m
It takes an object 4s to move 100m if it moves at 25m/s

Modules are very useful for bundling functionality, start using them and you will see great benefits in modularity with very little effort.

Extending class behavior using modules

Modules are a useful function storage, but their real power is their capacity to give classes new behavior. You can 'mix-in' a module into a class, immediately providing it new capabilities. This powerful mechanism removes the need to use multiple inheritance.

Understanding how it works is much easier with an example. Imagine we need to model 'taggability': an object can receive multiple tags and then print them. We can create a module that does this and then mix it into classes.

module Taggable
    def tags
        @tags ||= []
    end

    def add_tag(new_tag)
        tags << new_tag
    end

    def get_all_tags
        tags
    end

    def tag_count
        tags.count
    end
end

To mix-in a module into a class we use the 'Include' keyword. The following class models a very simple post (like a blog article, for example). We make it taggable by including the module as follows:

require_relative 'taggable'

class Post
    include Taggable

    def initialize(content, user)
        @content = content
        @user = user
    end

    def print_post_summary
        puts "By user: #{@user}"
        puts "Article: #{@content}"
        # We can use methods defined on the module
        puts "Tags: #{get_all_tags}"
    end
end

As you can notice on print_post_summary, we can use the get_all_tags method defined on the module. What's more, we can call on a Post instance the same module methods:

require_relative 'post'

first_post = Post.new('This is an article on how to use ruby modules', 'Juan Orozco Villalobos')
first_post.add_tag('Software development')
first_post.add_tag('Ruby programming')
first_post.add_tag('Programming languages')

first_post.print_post_summary
puts "We have a total of #{first_post.tag_count} tags"

Running this code shows the following result:

By user: Juan Orozco Villalobos
Article: This is an article on how to use ruby modules
Tags: ["Software development", "Ruby programming", "Programming languages"]
We have a total of 3 tags

Tagging other things

Imagine you need more classes with taggable behavior. Instead of writing the functionality again and again, you can just mix-in the module you already wrote. The following class represents a collection of related quotes. And yes, you guessed it, you also need them to have tags.

require_relative 'taggable'

class Quotes
    include Taggable

    def initialize()
        @quotes = []
    end

    def add_quote(quote)
        @quotes << quote
    end

    def print_all_quotes
        puts "The following quotes are classified as #{get_all_tags.join(', ')}:"
        @quotes.each do |quote| 
            puts quote  
        end
    end
end

Now let's give it a try:

require_relative 'post'
require_relative 'quotes'

# ...previous taggable usage

quotes = Quotes.new
quotes.add_tag('bullshit')
quotes.add_tag('pseudointellectual ')

quotes.add_quote('Wholeness unfolds through species specific marvel')
quotes.add_quote('The mind creates spontaneous happiness')
quotes.add_quote('The ego is an ingredient of the flow of balance')
quotes.add_quote('Perception fears karmic success')

quotes.print_all_quotes

When run, it prints:

The following quotes are classified as bullshit, pseudointellectual :
Wholeness unfolds through species specific marvel
The mind creates spontaneous happiness
The ego is an ingredient of the flow of balance
Perception fears karmic success

As a side note, you can find an awesome nonsense generator based on Deepak Chopra's twitter feed at wisdomofchopra.

The prepend keyword

Prepend works in a very similar way to include, in the sense it's used to mix-in functionality from a module into a class. The main difference happens when methods in the module and class have the same name.

Suppose you have a class and a module, both with the method print_description. If you include the module in the class and call print_description on an instance, you will call the method in the class. In other words, the class takes precedence over included modules.

On the other hand, if you prepend the module, when you call print_description, you will run the method in the module, as prepended modules take precedence over classes. In short, the order of precedence is something like:

  • Prepended modules
  • Class
  • Included modules

Of course, if you take into consideration inheritance it can get a bit more complicated, but just a little bit. The next section will deal with that.

Method lookup in an inheritance chain with modules.

In Ruby, invoking a method is usually called sending a message. For example, in quotes.print_all_quotes you are sending a print_all_quotes message to the quotes object. The quotes object knows how to respond to the print_all_quotes message and acts accordingly. Otherwise, your program will throw an exception letting you know that a given object doesn't know how to respond.

There are ways to give an object the ability to respond to messages its class didn't define. We just saw one of those ways: including a module. Another way is using inheritance: an object can respond to messages it's superclass defined.

When you send a message to an object, methods will be looked up in the following order:

1.Modules Prepended to its class
2.Its class
3.Modules Included in the class
4.Modules Prepended to its superclass
5.Its superclass
6.Modules Included in its superclass
7.Up in the inheritance hierarchy following this same pattern

Let's check an example to understand better how it works.

We have 2 classes and 4 modules, all of them have a method called print_greeting that just prints a greeting message on the console. One class inherits from the other, and the modules are included or prepended in them. I tried to provide names that make it clear where each of them fits, but for getting a better view of it, I made the following diagram.

method_resolution_graph

I also include in the diagram the Object and BasicObject classes for completeness.

Try it yourself and experiment a bit

In a ruby file, you will find the following lines, where we instantiate a ChildClass object and send the print_greeting message.

require_relative 'child_class'

test_object = ChildClass.new
test_object.print_greeting

Following the priority list above, you would expect the message printed would be the one on the ChildLevelModulePrepend, run the code to verify this is the case.

Hello, I am a method in ChildLevelModulePrepend, the one you prepended into the child class

Now, if you open the ChildLevelModulePrepend file and remove the definition of print_greeting, the next in the list would be the definition at the ChildClass. Perform the changes and check it follows the priority order.

You can continue testing with the code by removing definitions and seeing which method is called just to get a better feeling on how the method lookup works. Use the numbers under the boxes as a guide, I found this exercise very useful for gaining a better understanding of how method lookup works.

Using modules as namespaces

One last usage of modules I didn't want to leave out is when we use them to separate classes into their own namespaces.

Suppose you need to represent the class controller for both a video game console and a control panel. Instead of prefixing the classes with extra words in order to identify them, you can place them in modules for being sure what they represent.

You can define a module for the game console:

module GameConsole
    class Controller
        #Class Implementation
    end
end

And a module for the control panel

module ControlPanel
    class Controller
        #Class Implementation
    end
end

After that, when you create objects, you can specify which one you need, even if the classes share the same name:

require_relative 'game_console'
require_relative 'control_panel'

panel_controller = ControlPanel::Controller.new
console_controller = GameConsole::Controller.new

Using modules as namespaces is especially useful when you are writing your own gems. This way you create a well-defined boundary between the gem and the projects using it.

Modules are very useful

As you just read, modules can assist you in crafting better quality code in more than one way. I did my best to show you the most common scenarios, and hope you'll find this useful when working on your own projects.

What to do next:

  • Share this article with friends and colleagues. Thank you for helping me reach people who might find this information useful.
  • You can find the code samples here
  • You can find the official docs for modules here.
  • Check chapter 4 of The Well-Grounded Rubyist. This and other very helpful books are in the recommended reading list.
  • Send me an email with questions, comments or suggestions (it's in the About Me page). Come on, don't be shy!
Author image
Budapest, Hungary
Hey there, I'm Juan. A programmer currently living in Budapest. I believe in well-engineered solutions, clean code and sharing knowledge. Thanks for reading, I hope you find my articles useful!