Concerns in Rails are a powerful tool for extracting and organizing reusable pieces of code across your application. Let me explain this concept thoroughly.
A Concern is a module that extends ActiveSupport::Concern
and provides two main benefits:
Here's the anatomy of a typical Concern:
module Visible
extend ActiveSupport::Concern
included do
# Code to run when included (callbacks, validations, etc.)
scope :visible, -> { where(visible: true) }
end
class_methods do
# Class methods go here
def default_visibility
true
end
end
# Instance methods go here
def toggle_visibility
update(visible: !visible)
end
end
Rails has two standard directories for concerns:
app/models/concerns/
- For model-related mixinsapp/controllers/concerns/
- For controller-related mixinsInstead of repeating the same code in multiple models:
# Before
class Article < ApplicationRecord
scope :visible, -> { where(visible: true) }
def toggle_visibility
update(visible: !visible)
end
end
class Comment < ApplicationRecord
scope :visible, -> { where(visible: true) }
def toggle_visibility
update(visible: !visible)
end
end
# After with Concern
class Article < ApplicationRecord
include Visible
end
class Comment < ApplicationRecord
include Visible
end
Break large classes into logical components:
class User < ApplicationRecord
include Authenticatable
include Profileable
include Notifiable
end
module Taggable
extend ActiveSupport::Concern
class_methods do
def taggable_on(*fields)
fields.each do |field|
has_many :"#{field}_tags"
end
end
end
end
# Usage:
class Article < ApplicationRecord
include Taggable
taggable_on :category, :author
end
module Commentable
extend ActiveSupport::Concern
included do
has_many :comments
include Visible # Includes another concern
end
end
Test concerns in isolation using anonymous classes:
RSpec.describe Visible do
let(:model_class) do
Class.new(ApplicationRecord) do
self.table_name = 'articles'
include Visible
end
end
it 'adds visible scope' do
expect(model_class).to respond_to(:visible)
end
end
module Paginatable
extend ActiveSupport::Concern
class_methods do
def paginate(page: 1, per_page: 10)
page = [page.to_i, 1].max
offset((page - 1) * per_page).limit(per_page)
end
end
end
module Slugable
extend ActiveSupport::Concern
included do
before_validation :generate_slug
validates :slug, presence: true, uniqueness: true
end
def to_param
slug
end
private
def generate_slug
self.slug ||= title.parameterize if respond_to?(:title)
end
end
Резюмируем: Concerns in Rails provide a structured way to extract and share common functionality across your application. They help maintain DRY principles while keeping your code organized and modular. Use them for cross-cutting concerns that don't fit naturally into service objects or other patterns, but be mindful not to overuse them for core business logic.