In Ruby, it is possible to enhance the functionality of an existing object by including a module into a class using a mixin. It is commonplace to see code like this:
module DateMethods
def self.included(base)
base.extend ClassMethods
end
def calculate_interval
(Date.today - @elapsed_created).to_i
end
module ClassMethods
def interval; 3; end
end
end
class MyClass
include DateMethods
attr_accessor :started
def initialize(date:)
@started = Date.parse(date)
end
end
The example above is trivialized but hopefully helps to illustrate my observations. We have a module called ‘DateMethods’ which we are including into ‘MyClass’.
Within the ‘DateMethods’ module, the ‘self.included’ callback allows us to call ‘extend’ on the singleton object of ‘MyClass’ in order to extend the ‘ClassMethods’ module to create class methods.
While this approach works and it is something I have been doing myself, I can’t help but feel that this is not as syntactically pleasing as calling ‘MyClass.extend(DateMethods)’ which reads better.
We are also overriding ‘include’ to create an ‘extend’ when Ruby allows us to extend a module directly.
Rewriting the example above but using ‘extend’ instead:
module DateMethods
def self.extended(base)
base.include InstanceMethods
end
# class methods stay in the module body
def interval; 3; end
module InstanceMethods
def calculate_interval
(Date.today - @elapsed_created).to_i
end
end
end
class MyClass
attr_accessor :started
def initialize(date:)
@started = Date.parse(date)
end
end
MyClass.extend(DateMethods)
myclass = MyClass.new(date: "2015-05-28")
puts myclass.calculate_interval # => 1
puts MyClass.interval # => 3
Ruby provides a callback ‘extended’ which gets invoked whenever the receiver is used to extend an object. Within the ‘extended’ callback, we then ‘include’ the instance methods.
This is more flexible than ‘include’ itself as it can’t be called on instances directly. In addition, ‘include’ affects the entire class itself which may not be the intention if all you need is to have certain behavior at certain times. By using ‘extend’ on instances, we can achieve a kind of dynamic code loading:
module Pagination
def paginate(page = 1, items_per_page = size, total_items = size)
@page = 1
@items_per_page = items_per_page
@total_items = total_items
end
attr_reader :page, :items_per_page, :total_items
def total_pages
(total_items / items_per_page).ceil
end
end
collection = %w[first second third]
collection.extend(Pagination)
collection.paginate(1, 3, 10)
In the above example adapted from a James Earl Gray article on mixins, only the ‘collection’ object has the ‘Pagination’ module methods since the ‘extend’ is on the singleton/metaclass of ‘collection’ object itself, which restricts the scope of the ‘Pagination’ behavior to be contained and not affecting the entire class. We could just have easily swapped out ‘Pagination’ with another module at runtime.
In summary, I feel this is a more flexible approach to writing more maintable, dynamic code.
Happy hacking!