123456789_123456789_123456789_123456789_123456789_

Module: Module::Concerning

Relationships & Source Files
Extension / Inclusion / Inheritance Descendants
Included In:
::ActiveModel::SecurePassword::InstanceMethodsOnActivation, ActiveModel::Type::Helpers::AcceptsMultiparameterTime, ::ActiveModel::Validations::AcceptanceValidator::LazilyDefineAttributes, ActiveRecord::AttributeMethods::GeneratedAttributeMethods, ActiveRecord::Delegation::GeneratedRelationMethods, ActiveRecord::Enum::EnumMethods, ::ActiveSupport::Deprecation::DeprecatedConstantProxy, ::Module
Defined in: activesupport/lib/active_support/core_ext/module/concerning.rb

Overview

Bite-sized separation of concerns

We often find ourselves with a medium-sized chunk of behavior that we’d like to extract, but only mix in to a single class.

Extracting a plain old Ruby object to encapsulate it and collaborate or delegate to the original object is often a good choice, but when there’s no additional state to encapsulate or we’re making DSL-style declarations about the parent class, introducing new collaborators can obfuscate rather than simplify.

The typical route is to just dump everything in a monolithic class, perhaps with a comment, as a least-bad alternative. Using modules in separate files means tedious sifting to get a big-picture view.

Dissatisfying ways to separate small concerns

Using comments:

class Todo < ApplicationRecord
  # Other todo implementation
  # ...

  ## Event tracking
  has_many :events

  before_create :track_creation

  private
    def track_creation
      # ...
    end
end

With an inline module:

Noisy syntax.

class Todo < ApplicationRecord
  # Other todo implementation
  # ...

  module EventTracking
    extend ActiveSupport::Concern

    included do
      has_many :events
      before_create :track_creation
    end

    private
      def track_creation
        # ...
      end
  end
  include EventTracking
end

Mix-in noise exiled to its own file:

Once our chunk of behavior starts pushing the scroll-to-understand-it boundary, we give in and move it to a separate file. At this size, the increased overhead can be a reasonable tradeoff even if it reduces our at-a-glance perception of how things work.

class Todo < ApplicationRecord
  # Other todo implementation
  # ...

  include TodoEventTracking
end

Introducing Module#concerning

By quieting the mix-in noise, we arrive at a natural, low-ceremony way to separate bite-sized concerns.

class Todo < ApplicationRecord
  # Other todo implementation
  # ...

  concerning :EventTracking do
    included do
      has_many :events
      before_create :track_creation
    end

    private
      def track_creation
        # ...
      end
  end
end

Todo.ancestors
# => [Todo, Todo::EventTracking, ApplicationRecord, Object]

This small step has some wonderful ripple effects. We can

  • grok the behavior of our class in one glance,

  • clean up monolithic junk-drawer classes by separating their concerns, and

  • stop leaning on protected/private for crude “this is internal stuff” modularity.

Prepending concerning

#concerning supports a prepend: true argument which will prepend the concern instead of using include for it.

Instance Method Summary

Instance Method Details

#concern(topic, &module_definition)

A low-cruft shortcut to define a concern.

concern :EventTracking do
  #...
end

is equivalent to

module EventTracking
  extend ActiveSupport::Concern

  #...
end
[ GitHub ]

  
# File 'activesupport/lib/active_support/core_ext/module/concerning.rb', line 132

def concern(topic, &module_definition)
  const_set topic, Module.new {
    extend ::ActiveSupport::Concern
    module_eval(&module_definition)
  }
end

#concerning(topic, prepend: false, &block)

Define a new concern and mix it in.

[ GitHub ]

  
# File 'activesupport/lib/active_support/core_ext/module/concerning.rb', line 114

def concerning(topic, prepend: false, &block)
  method = prepend ? :prepend : :include
  __send__(method, concern(topic, &block))
end