Extending Ruby classes with modules
Composition in Ruby
5 min read
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:
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
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.
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
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
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 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
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]
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.