# frozen_string_literal: true
require "active_support/concern"
class Module
# = 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.
module Concerning
# Define a new concern and mix it in.
def concerning(topic, prepend: false, &block)
method = prepend ? :prepend : :include
__send__(method, concern(topic, &block))
end
# A low-cruft shortcut to define a concern.
#
# concern :EventTracking do
# ...
# end
#
# is equivalent to
#
# module EventTracking
# extend ActiveSupport::Concern
#
# ...
# end
def concern(topic, &module_definition)
const_set topic, Module.new {
extend ::ActiveSupport::Concern
module_eval(&module_definition)
}
end
end
include Concerning
end