Without the modules, you would have to rely on inheritance to organize your code and make it more reusable. Such an approach is far from being universal and a proper choice in every situation. Thanks to modules, we can extend classes more appropriately and flexibly.
This article is a dive into the include
, extend
and prepend
syntax that helps us organize our code and don’t repeat ourselves.
All my notes are based on years of hands-on experience at iRonin.IT - a top software development company, where we provide custom software development and IT staff augmentation services for a wide array of technologies.
A pinch of theory before practice
Before we start dealing with some practical examples to demonstrate how extend
, include
, and prepend
directives work, I have to introduce some essential information to give you a better understanding of how Ruby classes are performing in terms of class structure inheritance.
In the article, we will be using the following class:
class MyClass
def hello
puts 'Hello from my class'
end
end
It’s a dead-simple pure Ruby class that is perfect for demonstration purposes. When you create a class, it automatically inherits behavior from its ancestors.
Every class ancestors
As it illustrates the above image, our class inherits by default from three classes. This is essential information as using include
, extend
, or prepend
updates the inheritance structure of a given class.
If you would like to check ancestors of any class, you can use the following method:
MyClass.ancestors
# => [MyClass, Object, Kernel, BasicObject]
When calling this method on a class inside an immense legacy Ruby application, you might be surprised as the ancestors’ array can be much bigger, especially in the model classes.
We can move forward to understand how we can effectively extend any Ruby class.
Include
The include directive includes all methods from the given module and makes them available as instance methods in your class:
module Greeting
def hello
puts 'Hello from module'
end
end
class MyClass
include Greeting
end
my_class = MyClass.new
my_class.hello # => 'Hello from module'
If we would look into the ancestors of our class, we can spot the Greeting
constant:
MyClass.ancestors
=> [MyClass, Greeting, Object, Kernel, BasicObject]
When you execute the method, Ruby looks for the method definition using the class and its ancestors. If you would define the hello
method in the MyClass
, then the method from the Greeting
method won’t be executed unless you call super
. You can test this behavior by altering one of the parent classes of MyClass
:
class Object
def bye
puts "Bye from object"
end
end
MyClass.new.bye
# => Bye from object
I also mentioned that using super
allows us to execute the parent method:
class MyClass
def bye
puts "Bye from my class"
super
end
end
MyClass.new.bye
=> "Bye from my class"
=> "Bye from object"
Extend
The extend
directive includes all methods from the given module and make them available as class methods in your class:
module Greeting
def hello
puts 'Hello from module'
end
end
class MyClass
extend Greeting
end
MyClass.hello # => 'Hello from module'
What about the ancestors’ chain in the above case? It’s not modified. Instead, the ancestors’ chain for the Singleton
class is updated. Each class in Ruby also has the Singleton
class assigned.
We can look at Singleton’s class ancestors chain with the following method:
MyClass.singleton_class.ancestors
=> [#<Class:MyClass>, Greeting, #<Class:Object>, #<Class:BasicObject>, Class, Module, Object, Kernel, BasicObject]
Now that the Greeting
module has its own place and method defined, there will be called if no class method with the same name is specified in MyClass
.
Bonus: you can check given class’ instance methods using the following code:
MyClass.singleton_methods
=> [:hello]
Include and extend with one module
Before I move on to the prepend directive, we should stop for a second. You saw how you could add instance methods and class methods to your class from other modules in the previous examples.
If you would like to pack both instance and class methods inside one module and then add it to your classes, you have to modify the module a little bit:
module Greeting
module ClassMethods
def hello
puts "class hello"
end
end
def self.included(base)
base.extend(ClassMethods)
end
def hello
puts "instance hello"
end
end
Now we can execute the hello
method on the class and instance:
class MyClass
include Greeting
end
MyClass.hello # => 'class hello'
MyClass.new.hello # => 'instance hello'
It’s a common approach that provides flexibility and isolation at the same time. When inspecting ancestors of MyClass
, we can see that everything looks as expected:
MyClass.ancestors
# => [MyClass, Greeting, Object, Kernel, BasicObject]
MyClass.singleton_class.ancestors
# => [#<Class:MyClass>, Greeting::ClassMethods, #<Class:Object>, #<Class:BasicObject>, Class, Module, Object, Kernel, BasicObject]
Prepend
The last directive is prepend
that works similarly to the include
. The most significant difference is the order of included module in the ancestors’ chain. When you use include
, the module is placed right after your class, but when you use prepend is prepended, which means that it is set before your class:
module Greeting
def hello
puts "Hello from module"
super
end
end
class MyClass
prepend Greeting
def hello
puts "Hello from class"
end
end
MyClass.new.hello
# => "Hello from module"
# => "Hello from class"
You can say now that every class that prepends the Greeting
module becomes his parent, so you can call super
to call the method with the same name from the class that the module is pretending:
MyClass.ancestors
# => [Greeting, MyClass, Object, Kernel, BasicObject]
Didn't get enough of Ruby?
Check out our free books about Ruby to level up your skills and become a better software developer.