Skip to content

exAspArk/include_module

Repository files navigation

include_module

Build Status codecov Code Climate

There are some general problems with mixins, and a lot of people complain about them:

Using mixins like this is akin to “cleaning” a messy room by dumping the clutter into six separate junk drawers and slamming them shut. Sure, it looks cleaner at the surface, but the junk drawers actually make it harder to identify and implement the decompositions and extractions necessary to clarify the domain model.

Injecting methods into a class namespace is a bad idea for a very simple reason: every time you use a mixin, you are actually polluting your class namespace and losing track of the origin of your methods.

  • Mixins introduce implicit dependencies
  • Mixins cause name clashes
  • Mixins cause snowballing complexity

The include_module gem was created to help you to use mixing (aka module in Ruby) explicitly. It is a simple library with just 90 LOC, with zero dependencies, without any monkey patches and method overridings.

Usage

There are 2 common ways to use mixins in Ruby.

  1. Module.included(base)
module UserMixin
  def self.included(base)
    base.extend(ClassMethods)
    base.class_eval do
      belongs_to :account
    end
  end

  module ClassMethods
    def top_user
      User.order(rating: :desc).first
    end
  end

  def name
    "#{first_name} #{last_name}"
  end
end

class User
  include UserMixin
end

This is a standard way of using mixins in Ruby. The problem here is that when you look at the User you have no idea which methods the UserMixin defines in the class.

  1. ActiveSupport::Concern
module UserMixin
  extend ActiveSupport::Concern

  included do
    belongs_to :account
  end

  class_methods do
    def top_user
      User.order(rating: :desc).first
    end
  end

  def name
    "#{first_name} #{last_name}"
  end
end

class User
  include UserMixin
end

By extending your module, you add .class_methods which basically hides ClassMethods module inside, overrides .append_features and .included methods. Additionally it keeps tracking how many times .included was called to raise a custom exception about multiple "included" blocks.

But mostly ActiveSupport::Concern has the same problem as standard Module.included. Why can't we define explicitly which methods we would like to use from a mixin?

include_module

module UserMixin
  module ClassMethods
    def top_user
      User.order(rating: :desc).first
    end
  end

  INCLUDED = proc do
    belongs_to :account
  end

  def name
    "#{first_name} #{last_name}"
  end
end

class User
  extend IncludeModule

  extend_module UserMixin::ClassMethods, methods: [:top_user]
  include_module UserMixin, included: true, methods: [:name]
end

Now when you look at the class you can see which methods "included_module" adds in it. And it's all done without any monkey patches or method overridings.

It is almost in-place replacement for ActiveSupport::Concern. Here is a diff:

 module UserMixin
-  extend ActiveSupport::Concern
-
-  included do
+  INCLUDED = proc do
     belongs_to :account
   end

 ...

 class User
+  extend IncludeModule
+
-  include_module UserMixin
+  extend_module UserMixin::ClassMethods, methods: [:top_user]
+  include_module UserMixin, included: true, methods: [:name]
 end

You can also control which methods you would like to include, rename them while including in order to avoid name clashes, etc. Please see more examples below.

How Does It Work?

  • Include no methods

When you add extend IncludeModule, you add just 2 public methods called include_module and extend_module in your class.

class User
  extend IncludeModule
  include_module UserMixin
end

By default when you call include_module without any options, it won't load anything.

  • Include all methods

If you want to include everything from your module, you can pass the following options:

class User
  extend IncludeModule
  include_module UserMixin, included: true, methods: :all
end

It will include you module, extend your ClassMethods and class_eval your INCLUDED proc without any changes.

  • Include some methods

You can define explicitly which methods you want to include from you module:

module TestModule
  def foo
    :foo
  end

  def bar
    :bar
  end
end

class TestClass
  extend IncludeModule
  include_module TestModule, methods: [:foo]
end

In this case we have only TestClass#foo method implemented. It basically creates a new anonymous module which contains only specified methods and includes it as usual.

  • Rename included method
class User
  extend IncludeModule
  include_module UserMixin, methods: [name: :full_name]
end

We can rename methods while including a mixin by creating a new anonymous module as in the previous example. So User#full_name method will be available instead of User#name.

Installation

Install Ruby version >= 2.1.

Add this line to your application's Gemfile:

gem 'include_module'

And then execute:

$ bundle

Or install it yourself as:

$ gem install include_module

Development

Install development dependencies:

gem install bundler
bundle install

Run tests:

make test

To release a new version, update the version number in include_module.gemspec, and then run:

make release

Which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

What's New?

Please read the changelog.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/exAspArk/include_module.

To open a pull request:

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

License

The gem is available as open source under the terms of the MIT License.

About

Include modules explicitly! ☝️

Topics

Resources

License

Code of conduct

Stars

Watchers

Forks

Packages

No packages published