Modules are one of my favorite 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:
module Logging
def log(level, message)
File.open("log.txt", "a") do |f|
f.write "#{level}: #{message}"
end
end
end
class Service
include Logging
def do_something
begin
# do something
rescue StandardError => e
log :error, e.message
end
end
end
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.
class MyWorker
include Sidekiq::Worker
def perform(args)
# do some work
end
end
MyWorker.perform_async {something: "useful"}
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:
module Logging
# ...
end
class Service
include Logging
# ...
end
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:
module Logging
def log(message)
# log in a file
end
end
module Debug
def log(message)
# debug output
end
end
class Service
include Logging
include Debug
end
Service.ancestors # [Service, Debug, Logging, Object, ...]
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
.
Then, we could have called the method like this:
Service.log :info, "Something happened"
Often, you 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 a Module
, to also import class methods at runtime:
module Logging
module ClassMethods
def logging_enabled?
true
end
end
def self.included(base)
base.extend(ClassMethods)
end
def log(level, message)
# ...
end
end
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:
module ServiceDebugger
def run(args)
puts "Service run start: #{args.inspect}"
result = super
puts "Service run finished: #{result}"
end
end
class Service
prepend ServiceDebugger
# perform some real work
def run(args)
args.each do |arg|
sleep 1
end
{result: "ok"}
end
end
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 the method defined in the ServiceDebugger
module, and 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 to decorate the Service
implementation in a very simple and elegant way.