Type:
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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's 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.

1
2
3
4
5
6
class Todo
  # Other todo implementation
  # ...
 
  include TodoEventTracking
end

Introducing #concerning

By quieting the mix-in noise, we arrive at a natural, low-ceremony way to separate bite-sized concerns.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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.

concerning
  • References/Ruby on Rails/Rails/Classes/Module/Module::Concerning

concerning(topic, &block) Instance Public methods Define a new concern and

2025-01-10 15:47:30
concern
  • References/Ruby on Rails/Rails/Classes/Module/Module::Concerning

concern(topic, &module_definition) Instance Public methods A low-cruft shortcut

2025-01-10 15:47:30