-
Notifications
You must be signed in to change notification settings - Fork 21.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
A natural, low-ceremony way to separate responsibilities within a class. Imported from https://github.com/37signals/concerning#readme
- Loading branch information
Showing
4 changed files
with
219 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
136 changes: 136 additions & 0 deletions
136
activesupport/lib/active_support/core_ext/module/concerning.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |