How to use Modules and Mixins in Ruby

byGinkSat, 10 Apr 2021

1. Modules

Modules in Ruby

A module in Ruby is a collection of methods and constants that can be included in one or more classes. Modules are used to encapsulate related behavior and make it easy to reuse code between multiple classes. Modules can contain methods, constants, and other modules, and they can be used to provide a namespace for organizing code.

Some benefits that module provide:

1.1. Code organization:

module User
  class User
    # user class implementation
  end

  class Admin < User
    # admin class implementation
  end
end

In this example, the User module groups the User and Admin classes together, making it clear that they are related. This organization makes the code easier to maintain and understand.

1.2. Code reusability

module Greeting
  def say_hello
    puts "Hello, #{name}!"
  end
end

class Cat
  include Greeting
  attr_accessor :name
end

class Dog
  include Greeting
  attr_accessor :name
end

cat = Cat.new
cat.name = "Kitty"
cat.say_hello # => Hello, Kitty!

dog = Dog.new
dog.name = "Scooby-Doo"
dog.say_hello # => Hello, Scooby-Doo!

By including the module, the classes can share the same method implementation, without the need for inheritance.

1.3. Namespacing

module MyModule
  class MyClass
    # class implementation
  end
end

class MyClass
  # class implementation
end

MyModule::MyClass.new # creates an instance of the MyModule::MyClass class
MyClass.new           # creates an instance of the top-level MyClass class

By defining the class inside the module, it becomes possible to avoid naming conflicts with the top-level MyClass class.

1.4. Interfaces with modules

module CSV

  def to_csv
    raise "Not implemented"
  end

  def from_csv(line)
    raise "Not implemented"
  end

end

And then implement a class with that module

class Book
  include CSV

  def to_csv
    "#{@title},#{@page}"
  end

  def from_csv(line)
    parts = line.split(",")

    @title = parts[0]
    @page = parts[1]
  end

end

This looks and feels like interfaces in Java, however there is a problem. If we include the CSV module and forget to implement the to_csv method, we won’t notice this mistake until we run our code.

To fix it, let's try a different solution: Testing. We can create some set of tests and describe our object:

shared_examples "a CSV serializable object" do
  it { is_expected.to respond_to(:to_csv) }
  it { is_expected.to respond_to(:from_csv) }
end

describe Book do
  it_behaves_like "a CSV serializable object"
end

describe Sheet do
  it_behaves_like "a CSV serializable object"
end

With this way, we can implement as many interfaces as we want. No need to put all actions into one abstract class and extend from it.

2. Mixins

We went through all the benefits that module provides. Now let's get back to code reusability and dig deeper about the concept of Mixin.

A mixin is a type of module that is included in a class to add specific behavior to that class. A mixin is essentially a way to reuse code within a single class, rather than across multiple classes. When a mixin is included in a class, all of the methods defined in the mixin become available as methods in the class.

2.1. The difference between include and extend

  • include adds instance methods to a class. When a module is included in a class, the methods of the module become available to the instances of the class. In other words, the methods can be called on an object created from that class.
  • extend adds class methods to a class. When a module is extended in a class, the methods of the module become available to the class itself. In other words, the methods can be called on the class object itself, not on the instances of the class.
module MyModule
  def my_method
    puts "Hello from MyModule!"
  end
end

class MyClass
  include MyModule
end

MyClass.new.my_method #=> "Hello from MyModule!"

class AnotherClass
  extend MyModule
end

AnotherClass.my_method #=> "Hello from MyModule!"

2.2. The hooks when include or extend a module

  • In Ruby, self.included(base) is a hook method that is called whenever the module is included in another module or class. The base parameter in this method refers to the class or module that the module is being included in.
module MyModule
  def self.included(base)
    base.extend ClassMethods
  end

  module ClassMethods
    def my_class_method
      puts "This is a class method"
    end
  end

  def my_instance_method
    puts "This is an instance method"
  end
end

class MyClass
  include MyModule
end

MyClass.my_class_method
# Output: "This is a class method"

my_object = MyClass.new
my_object.my_instance_method
# Output: "This is an instance method"
  • A similar thing for extend, we also have self.extended
module MyModule
  def my_method
    puts "Hello from MyModule!"
  end

  def self.extended(base)
    puts "#{base} has extended MyModule!"
  end
end

class MyClass
  extend MyModule
end
# Output: MyClass has extended MyModule!

MyClass.my_method
# Output: Hello from MyModule!

The only thing we need to keep in mind is that everything in ruby is an object, including the class itself. And when extending, we do not apply the changes on class instances but on the class object.

3. Conclusions

With the help of include and extend methods, we have the ability to achieve code reuse and polymorphism in Ruby through the use of mixins. Mixins provide many benefits, including code organization and reusability, making them an important concept for Ruby developers to understand and use effectively.


© 2016-2024  GinkCode.com