Search code examples
ruby-on-railsruby-on-rails-5

Why use ActiveSupport::Concern instead of just a plain module?


I never understood why one has to use ActiveSupport::Concern is used for mixins instead of a plain module. Is there a simple answer to what ActiveSupport::Concern provides (at least in Rails 5) that a simple module without using ActiveSupport::Concern will do?


Solution

  • From https://api.rubyonrails.org/classes/ActiveSupport/Concern.html:

    A typical module looks like this:

    module M
      def self.included(base)
        base.extend ClassMethods
        base.class_eval do
          scope :disabled, -> { where(disabled: true) }
        end
      end
    
      module ClassMethods
        ...
      end
    end
    

    By using ActiveSupport::Concern the above module could instead be written as:

    require 'active_support/concern'
    
    module M
      extend ActiveSupport::Concern
    
      included do
        scope :disabled, -> { where(disabled: true) }
      end
    
      class_methods do
        ...
      end
    end
    

    Moreover, it gracefully handles module dependencies. Given a Foo module and a Bar module which depends on the former, we would typically write the following:

    module Foo
      def self.included(base)
        base.class_eval do
          def self.method_injected_by_foo
            ...
          end
        end
      end
    end
    
    module Bar
      def self.included(base)
        base.method_injected_by_foo
      end
    end
    
    class Host
      include Foo # We need to include this dependency for Bar
      include Bar # Bar is the module that Host really needs
    end
    

    But why should Host care about Bar's dependencies, namely Foo? We could try to hide these from Host directly including Foo in Bar:

    module Bar
      include Foo
      def self.included(base)
        base.method_injected_by_foo
      end
    end
    
    class Host
      include Bar
    end
    

    Unfortunately this won't work, since when Foo is included, its base is the Bar module, not the Host class. With ActiveSupport::Concern, module dependencies are properly resolved:

    require 'active_support/concern'
    
    module Foo
      extend ActiveSupport::Concern
      included do
        def self.method_injected_by_foo
          ...
        end
      end
    end
    
    module Bar
      extend ActiveSupport::Concern
      include Foo
    
      included do
        self.method_injected_by_foo
      end
    end
    
    class Host
      include Bar # It works, now Bar takes care of its dependencies
    end