Extending Ruby classes with modules
Composition in Ruby
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
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
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:
puts 'Hello from my class'
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
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, 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.
The include directive includes all methods from the given module and makes them available as instance methods in your class:
puts 'Hello from module'
my_class = MyClass.new
my_class.hello # => 'Hello from module'
If we would look into the ancestors of our class, we can spot the
=> [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
puts "Bye from object"
# => Bye from object
I also mentioned that using
super allows us to execute the parent method:
puts "Bye from my class"
=> "Bye from my class"
=> "Bye from object"
extend directive includes all methods from the given module and make them available as class methods in your class:
puts 'Hello from module'
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:
=> [#<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
Bonus: you can check given class’ instance methods using the following code:
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:
puts "class hello"
puts "instance hello"
Now we can execute the
hello method on the class and instance:
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, Greeting, Object, Kernel, BasicObject]
# => [#<Class:MyClass>, Greeting::ClassMethods, #<Class:Object>, #<Class:BasicObject>, Class, Module, Object, Kernel, BasicObject]
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:
puts "Hello from module"
puts "Hello from class"
# => "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:
# => [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.