Extending Ruby classes with modules

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 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.