mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Introduce Module#concerning
A natural, low-ceremony way to separate responsibilities within a class. Imported from https://github.com/37signals/concerning#readme
This commit is contained in:
parent
c28d0f2031
commit
1eee0ca6de
4 changed files with 219 additions and 0 deletions
|
@ -1,3 +1,50 @@
|
|||
* Introduce Module#concerning: a natural, low-ceremony way to separate
|
||||
responsibilities within a class.
|
||||
|
||||
Imported from https://github.com/37signals/concerning#readme
|
||||
|
||||
class Todo < ActiveRecord::Base
|
||||
concerning :EventTracking do
|
||||
included do
|
||||
has_many :events
|
||||
end
|
||||
|
||||
def latest_event
|
||||
...
|
||||
end
|
||||
|
||||
private
|
||||
def some_internal_method
|
||||
...
|
||||
end
|
||||
end
|
||||
|
||||
concerning :Trashable do
|
||||
def trashed?
|
||||
...
|
||||
end
|
||||
|
||||
def latest_event
|
||||
super some_option: true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
is equivalent to defining these modules inline, extending them into
|
||||
concerns, then mixing them in to the class.
|
||||
|
||||
Inline concerns tame "junk drawer" classes that intersperse many unrelated
|
||||
class-level declarations, public instance methods, and private
|
||||
implementation. Coalesce related bits and give them definition.
|
||||
These are a stepping stone toward future growth & refactoring.
|
||||
|
||||
When to move on from an inline concern:
|
||||
* Encapsulating state? Extract collaborator object.
|
||||
* Encompassing more public behavior or implementation? Move to separate file.
|
||||
* Sharing behavior among classes? Move to separate file.
|
||||
|
||||
*Jeremy Kemper*
|
||||
|
||||
* Fix file descriptor being leaked on each call to `Kernel.silence_stream`.
|
||||
|
||||
*Mario Visic*
|
||||
|
|
|
@ -4,6 +4,7 @@ require 'active_support/core_ext/module/anonymous'
|
|||
require 'active_support/core_ext/module/reachable'
|
||||
require 'active_support/core_ext/module/attribute_accessors'
|
||||
require 'active_support/core_ext/module/attr_internal'
|
||||
require 'active_support/core_ext/module/concerning'
|
||||
require 'active_support/core_ext/module/delegation'
|
||||
require 'active_support/core_ext/module/deprecation'
|
||||
require 'active_support/core_ext/module/remove_method'
|
||||
|
|
136
activesupport/lib/active_support/core_ext/module/concerning.rb
Normal file
136
activesupport/lib/active_support/core_ext/module/concerning.rb
Normal file
|
@ -0,0 +1,136 @@
|
|||
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
|
||||
# # Other todo implementation
|
||||
# # ...
|
||||
#
|
||||
# ## Event tracking
|
||||
# has_many :events
|
||||
#
|
||||
# before_create :track_creation
|
||||
# after_destroy :track_deletion
|
||||
#
|
||||
# private
|
||||
# def track_creation
|
||||
# # ...
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# == With an inline module:
|
||||
#
|
||||
# Noisy syntax.
|
||||
#
|
||||
# class Todo
|
||||
# # Other todo implementation
|
||||
# # ...
|
||||
#
|
||||
# module EventTracking
|
||||
# extend ActiveSupport::Concern
|
||||
#
|
||||
# included do
|
||||
# has_many :events
|
||||
# before_create :track_creation
|
||||
# after_destroy :track_deletion
|
||||
# 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
|
||||
# overhead feels in good proportion to the size of our extraction, despite
|
||||
# diluting our at-a-glance sense of how things really work.
|
||||
#
|
||||
# class Todo
|
||||
# # 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
|
||||
# # Other todo implementation
|
||||
# # ...
|
||||
#
|
||||
# concerning :EventTracking do
|
||||
# included do
|
||||
# has_many :events
|
||||
# before_create :track_creation
|
||||
# after_destroy :track_deletion
|
||||
# end
|
||||
#
|
||||
# private
|
||||
# def track_creation
|
||||
# # ...
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Todo.ancestors
|
||||
# # => Todo, Todo::EventTracking, 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.
|
||||
module Concerning
|
||||
# Define a new concern and mix it in.
|
||||
def concerning(topic, &block)
|
||||
include 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
|
||||
# include EventTracking
|
||||
def concern(topic, &module_definition)
|
||||
const_set topic, Module.new {
|
||||
extend ::ActiveSupport::Concern
|
||||
module_eval(&module_definition)
|
||||
}
|
||||
end
|
||||
end
|
||||
include Concerning
|
||||
end
|
35
activesupport/test/core_ext/module/concerning_test.rb
Normal file
35
activesupport/test/core_ext/module/concerning_test.rb
Normal file
|
@ -0,0 +1,35 @@
|
|||
require 'abstract_unit'
|
||||
require 'active_support/core_ext/module/concerning'
|
||||
|
||||
class ConcerningTest < ActiveSupport::TestCase
|
||||
def test_concern_shortcut_creates_a_module_but_doesnt_include_it
|
||||
mod = Module.new { concern(:Foo) { } }
|
||||
assert_kind_of Module, mod::Foo
|
||||
assert mod::Foo.respond_to?(:included)
|
||||
assert !mod.ancestors.include?(mod::Foo), mod.ancestors.inspect
|
||||
end
|
||||
|
||||
def test_concern_creates_a_module_extended_with_active_support_concern
|
||||
klass = Class.new do
|
||||
concern :Foo do
|
||||
included { @foo = 1 }
|
||||
def should_be_public; end
|
||||
end
|
||||
end
|
||||
|
||||
# Declares a concern but doesn't include it
|
||||
assert_kind_of Module, klass::Foo
|
||||
assert !klass.ancestors.include?(klass::Foo), klass.ancestors.inspect
|
||||
|
||||
# Public method visibility by default
|
||||
assert klass::Foo.public_instance_methods.map(&:to_s).include?('should_be_public')
|
||||
|
||||
# Calls included hook
|
||||
assert_equal 1, Class.new { include klass::Foo }.instance_variable_get('@foo')
|
||||
end
|
||||
|
||||
def test_concerning_declares_a_concern_and_includes_it_immediately
|
||||
klass = Class.new { concerning(:Foo) { } }
|
||||
assert klass.ancestors.include?(klass::Foo), klass.ancestors.inspect
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue