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!