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.