aboutsummaryrefslogtreecommitdiffstats
path: root/activesupport/lib/active_support/core_ext/module/concerning.rb
blob: 97b0a382cebdacd5f1bada1cf5f963459960f554 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
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
  # increased overhead can be a reasonable tradeoff even if it reduces our
  # at-a-glance perception of how things 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
    def concern(topic, &module_definition)
      const_set topic, Module.new {
        extend ::ActiveSupport::Concern
        module_eval(&module_definition)
      }
    end
  end
  include Concerning
end