super_module

by AndyObtiva

AndyObtiva / super_module

SuperModule allows defining class methods and method invocations the same way a super class does wit...

130 Stars 2 Forks Last release: Not found MIT License 141 Commits 10 Releases

Available items

No Items, yet!

The developer of this repository has not created any items for sale yet. Need a bug fixed? Help with integration? A different license? Create a request here:

SuperModule   SuperModule 1.4.1

Gem Version Build Status Coverage Status Code Climate

SuperModule enables developers to continue to use Ruby modules as first-class citizens with mixin inheritance even when wanting to inherit singleton-class methods and invocations.

Calling Ruby's

Module#include
to mix in a module does not bring in class methods by default. This can come as quite the surprise when attempting to include class methods via a module.

Ruby offers one workaround in the form of implementing the hook method

Module.included(base)
following a certain boilerplate code idiom. Unfortunately, it hinders code maintainability and productivity with extra unnecessary complexity, especially in production-environment projects employing many mixins (e.g. modeling business domain models with composable object traits).

Another workaround is

ActiveSupport::Concern
, a 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 helps improve readability a bit, it adds even more boilerplate idiom cruft, thus feeling no more than putting a band-aid on the problem.

But do not fear, SuperModule comes to the rescue! By declaring your module as a SuperModule, it will simply behave as one would expect and automatically include class methods along with instance methods, without any further work needed.

Used in my other project: Glimmer (Ruby Desktop GUI Library)

Works in both Ruby and JRuby.

Introductory Comparison

To introduce SuperModule, here is a comparison of three different approaches for writing a

UserIdentifiable
module, which includes ActiveModel::Model module as an in-memory alternative to
ActiveRecord::Base
superclass.

1) self.included(base)

module UserIdentifiable
  include ActiveModel::Model

def self.included(base) base.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 mostactiveuser) 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 no more than putting a band-aid on the problem. 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

By including

SuperModule
(following Ruby's basic convention of relying on a module), 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.

P.S. this library intentionally avoids bad techniques like "eval" of entire module body since they do not maintain Module mixin inheritance support. SuperModule supports full Ruby module mixin inheritance as it does not change it, yet only adds automation for singleton-class method inheritance on top of it (via surgical class_eval instead of full eval). SuperModule in fact encourages developers to continue to rely on basic Ruby code like

include SuperModule
.

Instructions

1) Install and require gem

Using Bundler

Add the following to Gemfile:

gem 'super_module', '1.4.1'

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) Simply include SuperModule at the top of your module definition before anything else.

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

Note: Even if you are including another super module in your new super module, you must

include SuperModule
at the top of your module definition before anything else.

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?

Usage Notes

  • 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

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.new
    instance has a singleton class that is a copy of the
    Object
    class, 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(...)
    or
    class << self
    enclosing
    def 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

IRB Example

Create a ruby file called supermoduleirb_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

=> [:userid, :creditcard_id]

media_authorization = MediaAuthorization.new # resulting object print-out varies

=> #MediaAuthorization:0x832b36be1

media_authorization.requirements_satisfied?

=> false

media_authorization.user_id = 387

=> 387

media_authorization.requirements_satisfied?

=> false

media_authorization.credit_card_id = 37

=> 37

media_authorization.requirements_satisfied?

=> true

Overriding
self.included(base)

With

SuperModule
, hooking into
self.included(base)
is no longer needed for most cases. Still, there rare exceptions where that might be needed to execute some meta-programmatic logic. Fortunately,
SuperModule
offers a mechanism to do so.

SuperModule
relies on
self.included(base)
, so modules mixing it in must refrain from implementing
self.included(base)
directly (
SuperModule
will automatically prevent that by providing instructions should one attempt to do so).

In order for a super module to hook into

self.included(base)
and add extra logic, it must do so via
super_module_included {|base| ... }
instead, which safely appends that logic to the work of
SuperModule
as well as other nested super modules.

Example:

module V1::SummarizedActiveModel
  include SuperModule

super_module_included do |klass| if klass.name.split(/::/).last.start_with?('Fake') klass.extend(FakeClassMethods1) end end

module FakeClassMethods1 def fake_summary 'This is a fake summary.' end end

class << self def self.validates(attribute, options = {}) validations << [attribute, options] end

def self.validations
  @validations ||= []
end

def summary
  validations.flatten.map(&amp;:to_s).join("/")
end

end end

module V1::ExtraSummarizedActiveModel include SuperModule

include ::V1::SummarizedActiveModel

super_module_included do |klass| if klass.name.split(/::/).last.start_with?('Fake') klass.extend(FakeClassMethods2) end end

module FakeClassMethods2 def fake_extra 'This is fake extra.' end end

class << self def extra "This is extra." end end end

class V1::SummarizedActiveRecord include ::V1::SummarizedActiveModel end

class V1::FakeSummarizedActiveRecord include ::V1::SummarizedActiveModel end

class V1::ExtraSummarizedActiveRecord include ::V1::ExtraSummarizedActiveModel end

class V1::FakeExtraSummarizedActiveRecord include ::V1::ExtraSummarizedActiveModel end

V1::SummarizedActiveRecord.validates 'foo', {:presence => true} V1::SummarizedActiveRecord.validates 'bar', {:presence => true} puts V1::SummarizedActiveRecord.summary

prints 'foo/{:presence=>true}/bar/{:presence=>true}'

V1::FakeSummarizedActiveRecord.validates 'foo', {:presence => true} V1::FakeSummarizedActiveRecord.validates 'bar', {:presence => true} puts V1::FakeSummarizedActiveRecord.summary

prints 'foo/{:presence=>true}/bar/{:presence=>true}'

puts V1::FakeSummarizedActiveRecord.fake_summary

prints 'This is a fake summary.'

V1::ExtraSummarizedActiveRecord.validates 'foo', {:presence => true} V1::ExtraSummarizedActiveRecord.validates 'bar', {:presence => true} puts V1::ExtraSummarizedActiveRecord.summary

prints 'foo/{:presence=>true}/bar/{:presence=>true}'

puts V1::ExtraSummarizedActiveRecord.extra

prints 'This is extra.'

V1::FakeExtraSummarizedActiveRecord.validates 'foo', {:presence => true} V1::FakeExtraSummarizedActiveRecord.validates 'bar', {:presence => true} puts V1::FakeExtraSummarizedActiveRecord.summary

prints 'foo/{:presence=>true}/bar/{:presence=>true}'

puts V1::FakeExtraSummarizedActiveRecord.fake_summary

prints 'This is a fake summary.'

puts V1::FakeExtraSummarizedActiveRecord.extra

prints 'This is extra.'

puts V1::FakeExtraSummarizedActiveRecord.fake_extra

prints 'This is fake extra.'

Limitations

SuperModule by definition has been designed to be used only in the initial code declaration of a module, not later mixing or re-opening of a module.

Change Log

CHANGELOG.md

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

TODO

None

Copyright

Copyright (c) 2014-2020 Andy Maleh. See LICENSE.txt for further details.

We use cookies. If you continue to browse the site, you agree to the use of cookies. For more information on our use of cookies please see our Privacy Policy.