SuperModule v1.1.1 [2015-04-09]
Calling Ruby's Module#include to mix in a module does not bring in class methods by default. This can come as quite a surprise whenever a developer attempts to include class methods via a module. Fortunately, Ruby does offer a solution in the form of implementing the hook method Module.included(base) following a certain boilerplate code idiom. However, this solution can hinder code maintainability and productivity flow in a big production-environment project that takes advantage of many mixins to model the business domain via composable object traits.
ActiveSupport::Concern is a popular Rails library that attempts to ease some of the boilerplate pain by offering a DSL layer on top of Module.included(base). Unfortunately, while it improves the readability of the code needed to include class methods, it supports the same boilerplate idiom, thus feeling no more than putting a band-aid on the problem.
Fortunately, SuperModule comes to the rescue. Including SuperModule at the top of a Ruby module's body automatically ensures inclusion of class methods whenever a developer mixes it in via Module#include.
Introductory Comparison
To introduce SuperModule, here is a comparison of three different approaches for writing a
UserIdentifiable module.
1) self.included(base)
module UserIdentifiable
include ActiveModel::Model
def self.included(base_klass)
base_klass.extend(ClassMethods)
base.class_eval do
belongs_to :user
validates :user_id, presence: true
end
end
module ClassMethods
def most_active_user
User.find_by_id(select('count(id) as head_count, user_id').group('user_id').order('count(id) desc').first.user_id)
end
end
def slug
"#{self.class.name}_#{user_id}"
end
end
This is a lot to think about and process for simply wanting inclusion of class method definitions (like most_active_user) and class method invocations (like belongs_to and validates). The unnecessary complexity gets in the way of problem-solving; slows down productivity with repetitive boiler-plate code; and breaks expectations set in other similar object-oriented languages, discouraging companies from including Ruby in a polyglot stack, such as Groupon's Rails/JVM/Node.js stack and SoundCloud's JRuby/Scala/Clojure stack.
2) ActiveSupport::Concern
module UserIdentifiable
extend ActiveSupport::Concern
include ActiveModel::Model
included do
belongs_to :user
validates :user_id, presence: true
end
module ClassMethods
def most_active_user
User.find_by_id(select('count(id) as head_count, user_id').group('user_id').order('count(id) desc').first.user_id)
end
end
def slug
"#{self.class.name}_#{user_id}"
end
end
A step forward that addresses the boiler-plate repetitive code concern, but is otherwise really just lipstick on a pig. To explain more, developer problem solving and creativity flow is still disrupted by having to think about the lower-level mechanism of running code on inclusion (using included) and structuring class methods in an extra sub-module (ClassMethods) instead of simply declaring class methods like they normally would in Ruby and staying focused on the task at hand.
3) SuperModule
module UserIdentifiable
include SuperModule
include ActiveModel::Model
belongs_to :user
validates :user_id, presence: true
def self.most_active_user
User.find_by_id(select('count(id) as head_count, user_id').group('user_id').order('count(id) desc').first.user_id)
end
def slug
"#{self.class.name}_#{user_id}"
end
end
With include SuperModule declared on top, developers can directly add class method invocations and definitions inside the module's body, and SuperModule takes care of automatically mixing them into classes that include the module.
As a result, SuperModule collapses the difference between extending a super class and including a super module, thus encouraging developers to write simpler code while making better Object-Oriented Design decisions.
In other words, SuperModule furthers Ruby's goal of making programmers happy.
Instructions
1) Install and require gem
Using Bundler
Add the following to Gemfile:
gem 'super_module', '1.0.0'
And run the following command:
bundle
Afterwards, SuperModule will automatically get required in the application (e.g. a Rails application) and be ready for use.
Using RubyGem Directly
Run the following command:
gem install super_module
(add --no-ri --no-rdoc if you wish to skip downloading documentation for a faster install)
Add the following at the top of your Ruby file:
require 'super_module'
2) Include SuperModule at the top of the module
module UserIdentifiable
include SuperModule
include ActiveModel::Model
belongs_to :user
validates :user_id, presence: true
def self.most_active_user
User.find_by_id(select('count(id) as head_count, user_id').group('user_id').order('count(id) desc').first.user_id)
end
def slug
"#{self.class.name}_#{user_id}"
end
end
3) Mix newly defined module into a class or another super module
class ClubParticipation < ActiveRecord::Base
include UserIdentifiable
end
class CourseEnrollment < ActiveRecord::Base
include UserIdentifiable
end
module Accountable
include SuperModule
include UserIdentifiable
end
class Activity < ActiveRecord::Base
include Accountable
end
4) Start using by invoking class methods or instance methods
CourseEnrollment.most_active_user
ClubParticipation.most_active_user
Activity.last.slug
ClubParticipation.create(club_id: club.id, user_id: user.id).slug
CourseEnrollment.new(course_id: course.id).valid?
Glossary and Definitions
- SuperModule: name of the library and Ruby module that provides functionality via mixin
- Super module: any Ruby module that mixes in SuperModule
- Singleton class: also known as the metaclass or eigenclass, it is the object-instance-associated class copy available to every object in Ruby (e.g. every
Object.newinstance has a singleton class that is a copy of theObjectclass, which can house instance-specific behavior if needed) - Singleton method: an instance method defined on an object's singleton class. Often used to refer to a class or module method defined on the Ruby class object or module object singleton class via
def self.method_name(...)orclass << selfenclosingdef method_name(...) - Class method invocation: Inherited Ruby class or module method invoked in the body of a class or module (e.g.
validates :username, presence: true) - Code-time: Time of writing code in a Ruby file as opposed to Run-time
- Run-time: Time of executing Ruby code
Usage Details
- SuperModule must always be included at the top of a module's body at code-time
- SuperModule inclusion can be optionally followed by other basic or super module inclusions
- A super module can only be included in a class or another super module
- SuperModule adds zero cost to instantiation of including classes and invocation of included methods (both class and instance)
IRB Example
Create a ruby file called super_module_irb_example.rb with the following content:
require 'rubygems' # to be backwards compatible with Ruby 1.8.7
require 'super_module'
module RequiresAttributes
include SuperModule
def self.requires(*attributes)
attributes.each {|attribute| required_attributes << attribute}
end
def self.required_attributes
@required_attributes ||= []
end
def requirements_satisfied?
!!self.class.required_attributes.reduce(true) { |result, required_attribute| result && send(required_attribute) }
end
end
class MediaAuthorization
include RequiresAttributes
attr_accessor :user_id, :credit_card_id
requires :user_id, :credit_card_id
end
Open irb (Interactive Ruby) and paste the following code snippets in. You should get the output denoted by the rockets (=>).
require './super_module_irb_example.rb'
=> true
MediaAuthorization.required_attributes
=> [:user_id, :credit_card_id]
= MediaAuthorization.new # resulting object print-out varies
=> #MediaAuthorization:0x832b36be1
.requirements_satisfied?
=> false
.user_id = 387
=> 387
.requirements_satisfied?
=> false
.credit_card_id = 37
=> 37
.requirements_satisfied?
=> true
How Does It Work?
Here is the general algorithm from the implementation:
def included(base)
__define_super_module_class_methods(base)
__invoke_super_module_class_method_calls(base)
end
1) Defines super module class methods on the including base class
For example, suppose we have a super module called Addressable:
module Addressable
include SuperModule
include Locatable
validates :city, presence: true, length: { maximum: 255 }
validates :state, presence: true, length: { is: 2 }
def self.merge_duplicates
# 1. Look through all Addressable instances in the database
# 2. Identify duplicates
# 3. Merge duplicate addressables
end
end
class Contact < ActiveRecord::Base
include Addressable
# … more code follows
end
This step ensures that merge_duplicates is included in Contact as a class method, allowing the call Contact.merge_duplicates
It does so by recording every class method defined using the Ruby self.singleton_method_added(method_name) hook, reading class method sources using the method_source gem, and finally upon invocation of self.included(base), class_evaling the recorded class methods on the including base class (or module).
In order to avoid interference with existing class method definitions, there is an exception list for what not to record, such as :included_super_modules, :class_eval, :singleton_method_added and any other "" prefixed class methods defined in SuperModule, such as super_module_class_method_calls.
Also, the recorded class method sources are altered to handle recording of method calls as well, which is used in the second step explained next.
2) Invoke super module class method calls on the including base class (or module).
For example, suppose we have a super module called Locatable:
module Locatable
include SuperModule
validates :x_coordinate, numericality: true
validates :y_coordinate, numericality: true
def move(x, y)
self.x_coordinate += x
self.y_coordinate += y
end
end
class Vehicle < ActiveRecord::Base
include Locatable
# … more code follows
end
This step guarantees invocation of the two Locatable validates method calls on the Vehicle object class.
It does so by relying on an interally defined method __record_method_call(method_name, *args, &block) to record every class method call that happens in the super module class body, and later replaying those calls on the including base class during self.included(base) by using Ruby's send(method_name, *args, &block) method introspection.
Limitations and Caveats
SuperModule has been designed to be used only in the code definition of a module, not to be mixed in at run-time.
Initial Ruby runtime load of a class or module mixing in SuperModule will incur a very marginal performance hit (in the order of nano-to-milliseconds). However, class usage (instantiation and method invocation) will not incur any performance hit, running as fast as any other Ruby class.
Given SuperModule's implementation relies on
self.included(base), if an including super module (or a super module including another super module) must hook intoself.included(base)for meta-programming cases that require it, such as conditionalincludestatements or method definitions, it would have to aliasself.included(base)and then invoke the aliased version in every super module that needs it like in this example:module AdminIdentifiable include SuperModule include UserIdentifiable class << self alias included_super_module included def included(base) included_super_module(base) # do some extra work # like conditional inclusion of other modules # or conditional definition of methods end endIn the future, SuperModule could perhaps provide robust built-in facilities for allowing super modules to easily hook into
self.included(base)without interfering with SuperModule behavior.
What's New?
v1.1.1
- Added support for private and protected methods
- Added many more RSpec test cases, including testing of empty and comment containing singleton methods
v1.1.0
- Brand new
self-friendly algorithm that ensures true mixing of super module singleton methods into the including base class or module, thus always returning the actual base class or moduleselfwhen invoking a super module inherited singleton method (thanks to Banister for reporting previous limitation on Reddit and providing suggestions) - New
included_super_modulesinherited singleton method that provides developer with a list of all included super modules similar to the Rubyincluded_modulesmethod. - No more use for method_missing (Thanks to Marc-André Lafortune for bringing up as a previous limitation in AirPair article reviews)
- New dependency on Banister's method_source library to have the self-friendly algorithm eval inherited class method sources into the including base class or module.
- Refactorings, including break-up of the original SuperModule into 3 modules in separate files
- More RSpec test coverage, including additional method definition scenarios, such as when adding dynamically via
class_evalanddefine_method
Feedback and Contribution
SuperModule is written in a very clean and maintainable test-first approach, so you are welcome to read through the code on GitHub for more in-depth details: https://github.com/AndyObtiva/super_module
The library is quite new and can use all the feedback and help it can get. So, please do not hesitate to add comments if you have any, and please fork the project on GitHub in order to make contributions via Pull Requests.
Articles, Publications, and Blog Posts
- 2015-04-05 - Ruby Weekly: Issue 240
- 2015-03-27 - AirPair Article: Step aside ActiveSupport::Concern. SuperModule is the new sheriff in town!
- 2014-03-27 - Code Painter Blog Post: Ruby SuperModule Comes To The Rescue!!
Copyright
Copyright (c) 2014-2015 Andy Maleh. See LICENSE.txt for further details.