Skip to content

Commit 1eee0ca

Browse files
committed
Introduce Module#concerning
A natural, low-ceremony way to separate responsibilities within a class. Imported from https://github.com/37signals/concerning#readme
1 parent c28d0f2 commit 1eee0ca

4 files changed

Lines changed: 219 additions & 0 deletions

File tree

activesupport/CHANGELOG.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,50 @@
1+
* Introduce Module#concerning: a natural, low-ceremony way to separate
2+
responsibilities within a class.
3+
4+
Imported from https://github.com/37signals/concerning#readme
5+
6+
class Todo < ActiveRecord::Base
7+
concerning :EventTracking do
8+
included do
9+
has_many :events
10+
end
11+
12+
def latest_event
13+
...
14+
end
15+
16+
private
17+
def some_internal_method
18+
...
19+
end
20+
end
21+
22+
concerning :Trashable do
23+
def trashed?
24+
...
25+
end
26+
27+
def latest_event
28+
super some_option: true
29+
end
30+
end
31+
end
32+
33+
is equivalent to defining these modules inline, extending them into
34+
concerns, then mixing them in to the class.
35+
36+
Inline concerns tame "junk drawer" classes that intersperse many unrelated
37+
class-level declarations, public instance methods, and private
38+
implementation. Coalesce related bits and give them definition.
39+
These are a stepping stone toward future growth & refactoring.
40+
41+
When to move on from an inline concern:
42+
* Encapsulating state? Extract collaborator object.
43+
* Encompassing more public behavior or implementation? Move to separate file.
44+
* Sharing behavior among classes? Move to separate file.
45+
46+
*Jeremy Kemper*
47+
148
* Fix file descriptor being leaked on each call to `Kernel.silence_stream`.
249

350
*Mario Visic*

activesupport/lib/active_support/core_ext/module.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
require 'active_support/core_ext/module/reachable'
55
require 'active_support/core_ext/module/attribute_accessors'
66
require 'active_support/core_ext/module/attr_internal'
7+
require 'active_support/core_ext/module/concerning'
78
require 'active_support/core_ext/module/delegation'
89
require 'active_support/core_ext/module/deprecation'
910
require 'active_support/core_ext/module/remove_method'
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
require 'active_support/concern'
2+
3+
class Module
4+
# = Bite-sized separation of concerns
5+
#
6+
# We often find ourselves with a medium-sized chunk of behavior that we'd
7+
# like to extract, but only mix in to a single class.
8+
#
9+
# Extracting a plain old Ruby object to encapsulate it and collaborate or
10+
# delegate to the original object is often a good choice, but when there's
11+
# no additional state to encapsulate or we're making DSL-style declarations
12+
# about the parent class, introducing new collaborators can obfuscate rather
13+
# than simplify.
14+
#
15+
# The typical route is to just dump everything in a monolithic class, perhaps
16+
# with a comment, as a least-bad alternative. Using modules in separate files
17+
# means tedious sifting to get a big-picture view.
18+
#
19+
# = Dissatisfying ways to separate small concerns
20+
#
21+
# == Using comments:
22+
#
23+
# class Todo
24+
# # Other todo implementation
25+
# # ...
26+
#
27+
# ## Event tracking
28+
# has_many :events
29+
#
30+
# before_create :track_creation
31+
# after_destroy :track_deletion
32+
#
33+
# private
34+
# def track_creation
35+
# # ...
36+
# end
37+
# end
38+
#
39+
# == With an inline module:
40+
#
41+
# Noisy syntax.
42+
#
43+
# class Todo
44+
# # Other todo implementation
45+
# # ...
46+
#
47+
# module EventTracking
48+
# extend ActiveSupport::Concern
49+
#
50+
# included do
51+
# has_many :events
52+
# before_create :track_creation
53+
# after_destroy :track_deletion
54+
# end
55+
#
56+
# private
57+
# def track_creation
58+
# # ...
59+
# end
60+
# end
61+
# include EventTracking
62+
# end
63+
#
64+
# == Mix-in noise exiled to its own file:
65+
#
66+
# Once our chunk of behavior starts pushing the scroll-to-understand it
67+
# boundary, we give in and move it to a separate file. At this size, the
68+
# overhead feels in good proportion to the size of our extraction, despite
69+
# diluting our at-a-glance sense of how things really work.
70+
#
71+
# class Todo
72+
# # Other todo implementation
73+
# # ...
74+
#
75+
# include TodoEventTracking
76+
# end
77+
#
78+
# = Introducing Module#concerning
79+
#
80+
# By quieting the mix-in noise, we arrive at a natural, low-ceremony way to
81+
# separate bite-sized concerns.
82+
#
83+
# class Todo
84+
# # Other todo implementation
85+
# # ...
86+
#
87+
# concerning :EventTracking do
88+
# included do
89+
# has_many :events
90+
# before_create :track_creation
91+
# after_destroy :track_deletion
92+
# end
93+
#
94+
# private
95+
# def track_creation
96+
# # ...
97+
# end
98+
# end
99+
# end
100+
#
101+
# Todo.ancestors
102+
# # => Todo, Todo::EventTracking, Object
103+
#
104+
# This small step has some wonderful ripple effects. We can
105+
# * grok the behavior of our class in one glance,
106+
# * clean up monolithic junk-drawer classes by separating their concerns, and
107+
# * stop leaning on protected/private for crude "this is internal stuff" modularity.
108+
module Concerning
109+
# Define a new concern and mix it in.
110+
def concerning(topic, &block)
111+
include concern(topic, &block)
112+
end
113+
114+
# A low-cruft shortcut to define a concern.
115+
#
116+
# concern :EventTracking do
117+
# ...
118+
# end
119+
#
120+
# is equivalent to
121+
#
122+
# module EventTracking
123+
# extend ActiveSupport::Concern
124+
#
125+
# ...
126+
# end
127+
# include EventTracking
128+
def concern(topic, &module_definition)
129+
const_set topic, Module.new {
130+
extend ::ActiveSupport::Concern
131+
module_eval(&module_definition)
132+
}
133+
end
134+
end
135+
include Concerning
136+
end
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
require 'abstract_unit'
2+
require 'active_support/core_ext/module/concerning'
3+
4+
class ConcerningTest < ActiveSupport::TestCase
5+
def test_concern_shortcut_creates_a_module_but_doesnt_include_it
6+
mod = Module.new { concern(:Foo) { } }
7+
assert_kind_of Module, mod::Foo
8+
assert mod::Foo.respond_to?(:included)
9+
assert !mod.ancestors.include?(mod::Foo), mod.ancestors.inspect
10+
end
11+
12+
def test_concern_creates_a_module_extended_with_active_support_concern
13+
klass = Class.new do
14+
concern :Foo do
15+
included { @foo = 1 }
16+
def should_be_public; end
17+
end
18+
end
19+
20+
# Declares a concern but doesn't include it
21+
assert_kind_of Module, klass::Foo
22+
assert !klass.ancestors.include?(klass::Foo), klass.ancestors.inspect
23+
24+
# Public method visibility by default
25+
assert klass::Foo.public_instance_methods.map(&:to_s).include?('should_be_public')
26+
27+
# Calls included hook
28+
assert_equal 1, Class.new { include klass::Foo }.instance_variable_get('@foo')
29+
end
30+
31+
def test_concerning_declares_a_concern_and_includes_it_immediately
32+
klass = Class.new { concerning(:Foo) { } }
33+
assert klass.ancestors.include?(klass::Foo), klass.ancestors.inspect
34+
end
35+
end

0 commit comments

Comments
 (0)