- Published on
Mastering Rails Concerns: DRY Up Your Code and Enhance Maintainability
- Authors
- Name
- Mohamed Hegab
- X
- @mohamedhegab92
Introduction
Concerns are a important feature in Rails that help you keep your code DRY (Don't Repeat Yourself). By using Concerns, you can make your models and controllers more readable and maintainable, which future you—or any other developer—will appreciate when revisiting the code. With Concerns, changes in logic only need to be made in one place, affecting all the places where the concern is used.
- What are Concerns?
- When to Use Concerns
- How to Create and Use a Concern
- Best Practices
- Common Pitfalls and How to Avoid Them
- Conclusion
What are Concerns?
Concerns in Rails are Ruby modules that are included or extended in multiple classes to share common logic. They allow you to encapsulate behavior that would otherwise be duplicated, providing a cleaner, more modular codebase.
This is the module you will write without the boilerplate for extending ActiveSupport::Concern
module M
def self.included(base)
base.extend ClassMethods
base.class_eval do
scope :disabled, -> { where(disabled: true) }
end
end
module ClassMethods
...
end
end
When you start a new Rails project, you’ll notice the presence of two directories: app/models/concerns/
and app/controllers/concerns/
. These are the default directories where Rails expects you to place your concerns, separating logic specific to models and controllers.
When to Use Concerns
A common scenario for using a concern is when you want to apply polymorphism in a model. For instance, if you have repeated code across multiple models, encapsulating this shared logic in a concern can prevent code duplication.
class Comment < ApplicationRecord
belongs_to :commentable, polymorphic: true
end
class Notice < ApplicationRecord
has_many :comments, as: :commentable
after_destroy do
comments.each(&:destroy)
end
end
class Sale < ApplicationRecord
has_many :comments, as: :commentable
after_destroy do
comments.each(&:destroy)
end
end
Other Cases for Using Concerns
- Shared Validations or Logic Across Models: When 2 or 3 models share the same validations or logic, concerns can help consolidate this into one place.
- Modular Components: If you want to separate components into modular parts, concerns allow you to encapsulate specific logic within these modules. This article delves deeper into this approach.
- Shared Methods in Controllers: If you have methods that are used across multiple controllers but don’t want them in
ApplicationController
, concerns offer a way to share these methods cleanly.
How to Create and Use a Concern
Step-by-Step Guide to Creating a Concern
For the example mentioned earlier, you could create a concern named Commentable
that handles the shared behavior across models. This concern would encapsulate the has_many :comments
association and any related methods.
module Commentable
extend ActiveSupport::Concern
included do
# any code that you want inside the class
# that includes this concern
has_many :comments, as: :commentable
after_destroy do
comments.each(&:destroy)
end
end
class_methods do
# methods that you want to create as
# class methods on the including class
end
prepended do
# Evaluate given block in context of base class,
# so that you can write class macros here.
# When you define more than one prepended block, it raises an exception
end
end
class Notice < ApplicationRecord
include Commentable
end
class Sale < ApplicationRecord
include Commentable
end
Once created, you can include this concern in any model where the behavior is needed.
The same approach applies to controllers if you need to share behavior across them.
Best Practices
Tips for Naming and Organizing Concerns
- Naming: For model concerns, a common convention is to use the suffix
-able
(e.g.,Commentable
). This is a pattern you’ll see in many codebases, such as GitLab's. - Controller Concerns: For controller concerns, the naming convention isn’t as strict, but typically, concerns handle specific tasks like parameter management or shared actions, reducing controller size and improving clarity. GitLab's controller concerns provide good examples.
Common Pitfalls and How to Avoid Them
- Overusing Concerns: Don’t use concerns just to make classes smaller. This can lead to code that's hard to trace and maintain.
- Naming Conflicts: Be mindful of naming conflicts between concerns and the classes they are included in.
- Bi-Directional Dependencies: Avoid situations where a concern depends on the including class for its logic. This can create tight coupling and make the code difficult to maintain.
Example for Bi-Directional Dependencies
module Printable
include ActiveSupport::Concern
def print
raise UnknownFormatError unless ['pdf', 'doc'].include?(@format)
# do print @content
end
end
class Document
include Printable
def initialize(format, content)
@format = format
@content = content
end
def export
# ...
print
end
end
The Document
class depends on Printable
and Printable
is using @format
from Document
this needs to be prevented in all costs.
Conclusion
Concerns are a powerful tool in Rails for keeping your code DRY, modular, and maintainable. While they can hide some methods and might not be immediately understandable to new developers, they offer significant benefits when used properly. I encourage you to experiment with Concerns in your projects if you haven’t already!