Extend, prepend and include in Ruby

In Ruby, the mechanisms of extend, prepend, and include are essential tools for adding functionality to classes. Each method serves a different purpose, allowing developers to tailor their code to specific needs.

Prepend

It is used to insert a module before the superclass in the inheritance chain. This means that the methods defined in the module will override any methods with the same name in the class and its ancestors.

module MyModule
  def greet
    puts "Hello from MyModule!"
  end
end

class MyClass
  prepend MyModule

  def greet
    puts "Hello from MyClass!"
  end
end

obj = MyClass.new
obj.greet
# Output: Hello from MyModule!

Extend

It is used to add methods from a module as class methods, effectively extending the class with the functionality of the module.

module MyModule
  def greet
    puts "Hello from MyModule!"
  end
end

class MyClass
  extend MyModule
end

MyClass.greet
# Output: Hello from MyModule!

Let's look at a larger usage example:

# extend
module MyValidators
  class PresenceValidator
    attr_reader :attr_name
    def initialize(attr_name)
      @attr_name = attr_name
    end

    def valid?(value)
      return false if value.nil? || value == ''
      true
    end

    def error_msg
      "#{attr_name} must be present"
    end
  end

  class NumberValidator
    attr_reader :attr_name
    def initialize(attr_name)
      @attr_name = attr_name
    end

    def valid?(value)
      return false unless value.is_a?(Integer)

      true
    end

    def error_msg
      "#{attr_name} must be number"
    end
  end
  
  def validators
    @validators ||= []
  end

  def errors
    @errors ||= []
  end

  def validates_presence_of(*attributes)
    attributes.each do |attribute|
      validators << PresenceValidator.new(attribute)
    end
  end

  def validates_numericality_of(*attributes)
    attributes.each do |attribute|
      validators << NumberValidator.new(attribute)
    end
  end

  def self.extended(base)
    base.include MyInstanceMethods
  end

  module MyInstanceMethods
    def validate
      self.class.validators.each do |validator|
        unless validator.valid?(public_send(validator.attr_name))
          self.class.errors << validator.error_msg
        end
      end
    end

    def save
      validate

      unless self.class.errors.empty?
        raise "SORRY, invalid data: #{self.class.errors}"
      end

      puts "saved!"
    end
  end
end

class User
  extend MyValidators

  attr_accessor :name, :email, :age
  validates_presence_of :name, :email
  validates_numericality_of :age

  def initialize(name: '', email: '', age: nil)
    @name = name
    @email = email
    @age = age

    puts 'initialized'
  end

  def print
    puts "Name: #{name}, Email: #{email}, age: #{age}"
  end
end

user = User.new(name: 'abcd', email: 'asdf', age: 1)
user.save
user.print
User.new(name: 'abcd', email: 'asdf', age: nil).save

This segment defines a module called MyValidators that encapsulates two validator classes, PresenceValidator and NumberValidator. These classes contain logic to determine the validity of attributes and provide corresponding error messages.
The validators and errors methods within the module are used to store validation instances and error messages, respectively. The validates_presence_of and validates_numericality_of methods contribute to the accumulation of new validator instances in the validators collection.
The self.extended method is a hook method that comes into play when the module is extended to a class. In this context, it adds another module named MyInstanceMethods to the class that extends MyValidators. This gives the class access to the instance methods defined in MyInstanceMethods.
In this section, the User class extends the MyValidators module. As a result, the class inherits the methods of the module as class methods. It also specifies validations for the Name, Email, and Age attributes using the methods provided by the module.
Finally, the code creates instances of the User class, sets attributes, and demonstrates how the validation and storage mechanisms work. The first instance is valid and is successfully saved, while the second instance is missing the age information, resulting in an error message.

Include

It is used to merge the methods of a module into a class. When you include a module in a class, its methods become instance methods of that class. This allows the class to access the functionality provided by the module.

module MyModule
  def greet
    puts "Hello from MyModule!"
  end
end

class MyClass
  include MyModule
end

obj = MyClass.new
obj.greet
# Output: Hello from MyModule!

In this example, by including MyModule in MyClass, the greet method of MyModule becomes available to instances of MyClass. Let's see an example of using this method.

# include
module MyValidators
  class PresenceValidator
    attr_reader :attr_name
    def initialize(attr_name)
      @attr_name = attr_name
    end

    def valid?(value)
      return false if value.nil? || value == ''
      true
    end

    def error_msg
      "#{attr_name} must be present"
    end
  end

  class NumberValidator
    attr_reader :attr_name
    def initialize(attr_name)
      @attr_name = attr_name
    end

    def valid?(value)
      return false unless value.is_a?(Integer)

      true
    end

    def error_msg
      "#{attr_name} must be number"
    end
  end

  def self.included(klass)
    klass.extend(ClassMethods)
  end

  module ClassMethods
    def validators
      @validators ||= []
    end

    def errors
      @errors ||= []
    end

    def validates_presence_of(*attributes)
      attributes.each do |attribute|
        validators << PresenceValidator.new(attribute)
      end
    end

    def validates_numericality_of(*attributes)
      attributes.each do |attribute|
        validators << NumberValidator.new(attribute)
      end
    end
  end

  def validate
    self.class.validators.each do |validator|
      unless validator.valid?(public_send(validator.attr_name))
        self.class.errors << validator.error_msg
      end
    end
  end

  def save
    validate

    unless self.class.errors.empty?
      raise "SORRY, invalid data: #{self.class.errors}"
    end

    puts "saved!"
  end
end

class User
  include MyValidators

  attr_accessor :name, :email, :age
  validates_presence_of :name, :email
  validates_numericality_of :age

  def initialize(name: '', email: '', age: nil)
    @name = name
    @email = email
    @age = age

    puts 'initialized'
  end

  def print
    puts "Name: #{name}, Email: #{email}, age: #{age}"
  end
end

user = User.new(name: 'abcd', email: 'asdf', age: 1)
user.save
user.print
User.new(name: 'abcd', email: 'asdf', age: nil).save

This example defines a module called MyValidators that encapsulates two validator classes, PresenceValidator and NumberValidator(similar to above). These classes contain logic to determine the validity of attributes, as well as methods to provide error messages if the validation fails.
The validators and errors methods within the module are used to store validation instances and error messages, respectively. The validates_presence_of and validates_numericality_of methods are responsible for adding new validator instances to the validators collection.
The self.extended method inside the module is a hook method that is triggered when the module is extended to a class. In this case, it includes another module named MyInstanceMethods in the class that extends MyValidators.
In this section, the User class extends the MyValidators module. This means that the class gets access to the methods of the module as class methods. It also specifies validations for the name, email, and age attributes using the validates_presence_of and validates_numericality_of methods provided by the module.
Finally, the code creates instances of the User class, sets attributes, and demonstrates how the validation and storage mechanisms work. The first instance is valid and saved, while the second instance has missing age information, resulting in an error message.

Class

What about a base class? Well, we can do the same use as our modules from above using standard class inheritance.

# class inheritance
class PresenceValidator
  attr_reader :attr_name
  def initialize(attr_name)
    @attr_name = attr_name
  end

  def valid?(value)
    return false if value.nil? || value == ''
    true
  end

  def error_msg
    "#{attr_name} must be present"
  end
end

class NumberValidator
  attr_reader :attr_name
  def initialize(attr_name)
    @attr_name = attr_name
  end

  def valid?(value)
    return false unless value.is_a?(Integer)

    true
  end

  def error_msg
    "#{attr_name} must be number"
  end
end

class Base
  class << self
    def validators
      @validators ||= []
    end

    def errors
      @errors ||= []
    end

    def validates_presence_of(*attributes)
      attributes.each do |attribute|
        validators << PresenceValidator.new(attribute)
      end
    end

    def validates_numericality_of(*attributes)
      attributes.each do |attribute|
        validators << NumberValidator.new(attribute)
      end
    end
  end

  def validate
    self.class.validators.each do |validator|
      unless validator.valid?(public_send(validator.attr_name))
        self.class.errors << validator.error_msg
      end
    end
  end

  def save
    validate

    unless self.class.errors.empty?
      raise "SORRY, invalid data: #{self.class.errors}"
    end

    puts "saved!"
  end
end

class User < Base
  attr_accessor :name, :email, :age
  validates_presence_of :name, :email
  validates_numericality_of :age

  def initialize(name: '', email: '', age: nil)
    @name = name
    @email = email
    @age = age

    puts 'initialized'
  end

  def print
    puts "Name: #{name}, Email: #{email}, age: #{age}"
  end
end

user = User.new(name: 'abcd', email: 'asdf', age: 1)
user.save
user.print

This section like from the previous example of code defines two validator classes, PresenceValidator and NumberValidator. Each class contains logic to determine the validity of attributes and provide corresponding error messages.
The Base class is introduced as a base class from which other classes can inherit. It contains class methods such as validators, errors, validates_presence_of, and validates_numericality_of. These methods make it easy to accumulate validation instances and error messages at the class level.
The validate and save methods within the Base class are designed to perform validation checks on attributes and manage the save process. The validate method iterates through registered validators to determine attribute validity, and the save method triggers validation before attempting to save. If validation errors are detected, an error message is raised.
In this segment, the User class inherits from the Base class. This inheritance gives the User class access to the class methods defined in the Base class, including validators, errors, and the validation methods.
The User class specifies validations for the Name, Email, and Age attributes using the inherited validates_presence_of and validates_numericality_of methods. It also includes an initialize method to set attribute values and a print method to display attribute information.
An instance of the User class is created with the specified attributes, and the save method is called to trigger validation and saving. If the instance passes validation, it is successfully saved. The print method is then called to display the attributes.

Summary

Unlike prepend, the methods of the included module are added after the class methods in the inheritance chain. If a method with the same name already exists in the class or its ancestors, the method from the included module overrides it. However, if you want the methods of the module to override the methods of the class, you can use prepend instead.

Here's a quick summary of the differences:

  • include: Adds module methods as instance methods to the class.
  • extend: Adds module methods as class methods to the class.
  • prepend: Inserts module before the superclass in the inheritance chain, allowing module methods to override class methods.

Happy coding!