Modules are one of the most interesting features of Ruby. You can use them to attach specific behavior on your classes, and to organize your code using composition rather than inheritance. Here is a simple example:
Also, a lot of gems make use of modules to organize their code and ease up integration into your application. For example the Sidekiq gem provides the Sidekiq::Worker module to attach behavior to custom classes and use them as asynchronous workers components.
Though include is the most common way of importing external code into a class, Ruby provides also two other ways to achieve that: extend and prepend. However, they don?t have the same behavior at all, and these differences are often misunderstood by Ruby developers.
To understand how to use them, we must first have a deeper look into how Ruby is resolving methods to execute at runtime, using something called the ancestors chain.
The Ancestors chain
When a Ruby class is created, it holds a list of constant names which are its ancestors. They are all the classes that the class inherits from, and the modules they include. For example, by calling ancestors on the String class, we get the list of its ancestors:
> String.ancestors=> [String, Comparable, Object, PP::ObjectMixin, Kernel, BasicObject]
We can see at the top of the chain BasicObject, which is the root of the Ruby object hierarchy, and also Object, the superclass of all classes, that also includes the Kernel module.
When we call the method object_id on a String instance (or any other class), Ruby will look up through the class ancestors to find the object_id method, and will eventually find it defined on the Object class.
When calling a method that is not defined anywhere, Ruby won?t find the method in any of the classes or modules in the ancestors chain, and will end up calling method_missing of BasicObject, which gives a last chance for the developer to execute fallback code.
Knowing the basics about the ancestors chain of Ruby classes, we can now look at the different ways of importing modules.
Include
include is the most used and the simplest way of importing module code. When calling it in a class definition, Ruby will insert the module into the ancestors chain of the class, just after its superclass. Going back at our first example:
If we look at the ancestors of the Service class, we can see that the Logging module is present just between the class itself and its direct superclass, which is Object.
> Service.ancestors=> [Service, Logging, Object, …]
That?s why we can call methods defined in the module on the class instances. Ruby, not finding these methods on the class, will go one step up on the chain to find them on the module.
Also, it is worth noting that, when including two modules or more, the last included one will always be inserted again right between the class and the rest of the chain:
So, in case of a method conflict like on this example, the first module to respond in the ancestors chain would be the last included module, Debug.
Extend
On the other end, using extend on a class will actually import the module methods as class methods. If we have used extend rather than include in our example, the Logging module would not have been inserted into the Service class ancestors chain. So, we couldn?t have called the log method on any Service instance.
Instead, Ruby would have inserted the module in the ancestors chain of the singleton class of the Service class. This singleton class (named #Service) is actually where the class methods of Service are defined. The methods of the module Logging would then have been available as class methods of Service.
If you want to know a bit more about singleton classes, I wrote another post diving a bit deeper in the topic here.
Then, we could have called the method like this:
Service.log :info, “Something happened”
Often, you will want to use a module to import instance methods on a class, but at the same time to define class methods. Normally, you would have to use two different modules, one with include to import instance methods, and another one with extend to define class methods.
A common idiom to achieve that using a single module is to use the included hook method of Module, to also import class methods at runtime:
Now, when we include the module into the Service class, the module methods will be imported as instance methods of the class. The included method is also called, with the including class as an argument. We can then call extend on it to import the methods of the ClassMethods submodule as class methods. The circle is complete.
Prepend
Available since Ruby 2, prepend is a bit less known to Rubyists than its two other friends. It actually works like include, except that instead of inserting the module between the class and its superclass in the chain, it will insert it at the bottom of the chain, even before the class itself.
What it means is that when calling a method on a class instance, Ruby will look into the module methods before looking into the class. This difference of behavior allows you to decorate existing classes with modules and implement ?around? logic:
Using prepend, the module ServiceDebugger is now inserted at the very bottom of the ancestors chain.
You can verify it yourself again by calling Service.ancestors:
> Service.ancestors=> [ServiceDebugger, Service, Object, …]
Calling run on a Service instance will execute first the method defined in the ServiceDebugger module. We can use super to call the same method on the direct ancestor up the chain, which is the Service class itself. We took advantage of this behavior to decorate the Service implementation in a very simple and elegant way.
Thank you for reading, and happy Ruby coding!
This post has been published on my blog as well.If you liked this post, don?t forget to ? 🙂