Conquering Code Clutter - A Guide to Rails Concerns
As your Rails application grows, code starts to sprawl. Similar functionalities creep into different models and controllers, leading to copy-paste nightmares and a maintenance headache. This is where Rails Concerns can be exploited to banish duplication and organize your codebase.
What are Rails Concerns?
Imagine a reusable module with methods
and
logic applicable to multiple places in your code.
That's a Rails Concern – a code block extracted from specific models
or
controllers
and
shared across your application.
It is a library of specialized tools readily available to any class
that needs them.
Fresh Rails projects come with special folders called concerns
inside both controllers
and
models
.
They're ready to store reusable code snippets.
In simple terms,
a Rails Concern is a module that extends the ActiveSupport::Concern
module.
In Rails,
modules are like special containers for organizing
and
sharing code.
Modules serve two primary purposes:
- Namespace Management
- Code Sharing (Mixins)
While both modules and concerns are used for code organization and reusability in Ruby (and specifically Rails), there are some key differences:
-
Modules primarily focus on grouping related code logically and preventing name conflicts. They can hold any kind of code, like methods, constants, and even other modules. Concerns are designed for code reuse and encapsulating functionality related to a specific aspect or behaviour within a Rails application. They generally hold methods and hooks intended to be included in models or controllers.
-
Modules can be used more flexibly, included or extended (adding methods directly) to a class. They can also be nested within other modules. Concerns often rely on the
ActiveSupport::Concern
mixin to provide additional features like class methods and hooks. They're mainly included in models and controllers using theinclude
keyword. -
Modules are more general-purpose and can group any related code within the application, regardless of its specific intent. Concerns are more targeted, focusing on specific functionalities like authentication, validation, or authorization. They're designed to address common problems and promote organization within Rails applications.
A concern can be implemented as below:
module Archiveable
extend ActiveSupport::Concern
included do
scope :archive, -> { where(status: 'archive') }
end
class_methods do
...
end
end
If you convert the above concern to a module, it will look as below:
module Archiveable
def self.included(base)
base.extend ClassMethods
base.class_eval do
scope :archive, -> { where(status: 'archive') }
end
end
module ClassMethods
...
end
end
In the code above, by grouping related methods, Rails Concerns significantly enhances code organization, leading to a cleaner and more maintainable codebase.
Why Use Rails Concerns?
-
DRY (Don't Repeat Yourself): Eliminate copy-pasted code, reducing redundancy and maintenance work.
-
Improved Organization:
Group related functionalities into logical units, enhancing code clarity and readability. -
Modular Architecture: Promote a cleaner and more modular codebase, making it easier to understand and manage.
-
Code Reusability: Leverage common logic across different parts of your application, saving time and effort.
When to Use Rails Concerns?
Here are some situations where Rails concerns shine:
-
Shared Functionality: When two or more models share similar behaviour, like authorization or validation logic, extract it into a concern. This avoids code duplication and promotes DRY (Don't Repeat Yourself) principles.
-
Code Organization: If a model is bloated with unrelated methods, group them into concerns based on function. This enhances clarity and prevents your model from becoming a catch-all for diverse functionalities.
-
Cross-Model Logic: Concerns are handy for encapsulating logic that spans multiple models, like file attachment handling or image processing. This keeps your models focused and avoids scattering related code across different places.
-
Reusability: If you anticipate needing the same functionality in various places, a Rails concern acts as a reusable module. This saves you time and effort while ensuring consistency across your codebase.
-
Testability: Isolating logic in concerns simplifies testing. You can test the concern independently of other models, leading to more precise and focused test cases.
Writing Effective Rails Concerns
-
Focus on a single responsibility: Each Concern should address a specific task or functionality. For example, if your application validates the user's email and authenticates the user, the validation and authentication should be handled in two different ways.
require "active_support/concern" module ValidateAndAuthenticateUser extend ActiveSupport::Concern included do # code to validate the email # code to authenticate the user ... end class_methods do ... end end
Instead of clubbing the two functionalities into one concern, they should be split into two.
require "active_support/concern" module Validatable extend ActiveSupport::Concern included do ... end class_methods do ... end end # and module Authenticatable extend ActiveSupport::Concern included do ... end class_methods do ... end end
-
Keep it small and concise: Avoid overloading concerns; maintain modularity and clarity.
Let's say your application has a
Post
model, and you want to implement the search operation; you can create aSearchable
concern with basic functionality like search Post based ontext
anddescription
.module Searchable extend ActiveSupport::Concern def search(query) # Base search logic end end
You can include this concern in the
Post
model. If you need to add any additional conditions on the search functionality which is related only to Post, you can add asearch
method to the Post model.class Post < ApplicationRecord include Searchable def search(query) super(query).where(active: true) # Add additional conditions end end
The advantage of this is the
Searchable
concern is generic and can be used in another model likeComment
. If you keep addingPost
model-related methods to theSearchable
concern, it no longer remains generic. -
Use descriptive names: Make it easy to understand what the concern does by using clear and informative names.
For example, if the concern deals only with validating the email, it can be called
EmailValidatable
instead ofValidatable
. -
Document your code: Add comments explaining the concern's purpose and usage.
This not only helps in understanding what the concern does but also helps the developer understand if it can be included in other models or not.
Getting Started with Rails Concerns
Rails provides dedicated concerns
folders within both models
and
controller
directories.
Tuck model-related concerns in app/models/concerns
and
controller-specific ones in app/controllers/concerns
.
Let's take an example of a Rails application with a Post model,
and
each post has a status
column.
The status of the post can be draft
,
ready_to_publish
,
published
or
deleted
.
The status change is handled via
AASM
gem.
The whole functionality can be pulled into PostStatusable
concern
as shown below:
# app/models/concerns/post_statusable.rb
module PostStatusable
extend ActiveSupport::Concern
include AASM
aasm do
state :draft, initial: true
state :ready_to_publish, :published, :deleted
event :approve do
transitions from: :draft, to: :ready_to_publish
end
event :publish do
transitions from: :ready_to_publish, to: :published
end
event :delete do
transitions from: [:draft, :ready_to_publish, :published], to: :deleted
end
end
end
# app/models/post.rb
class Post < ApplicationRecord
include PostStatusable
end
# rails console
post = Post.create!(
title: "First post",
description: "First post description"
)
post.draft?
=> true
post.approve
post.draft?
=> false
post.ready_to_publish?
=> true
A few other examples where Rails concerns can be used are:
-
Authenticateable Concern: Handles user authentication logic shared across User and Admin models.
-
Validatable Concern: Provides standard validation methods for various models.
-
Loggable Concern: Adds logging functionality to models or controllers.
Rails Concerns: The Flip Side of the Coin
While Rails Concerns offer undeniable benefits in code organization and reusability, it's essential to be aware of their potential drawbacks:
-
Over-engineering: Overzealous use of Rails concerns can lead to a cohesive codebase, making it easier to understand and navigate. Start small and only extract logic that genuinely deserves its module.
-
Name confusion: With multiple Rails concerns, naming conflicts can become a hassle. Ensure clear and descriptive names to avoid accidental overrides and maintain readability.
-
Testing complexity: Testing code across multiple concerns can be intricate. Plan your test approach carefully to guarantee complete coverage and avoid hidden bugs.
-
Hidden dependencies: Code within concerns might rely on implicit dependencies from the including class. Explicitly documenting and testing these dependencies ensures smooth implementation and prevents unexpected issues.
-
Increased learning curve: Mastering Concerns effectively requires a more profound understanding of Ruby and Rails internals. This can be a barrier for beginners, so prioritize gradual learning and clarity within your codebase.
Rails concerns, once mastered, become powerful tools in your development arsenal. They help you conquer code clutter, maintain a clean and organized codebase, and craft elegant and maintainable Rails applications. So, arm yourself with this knowledge and prepare to write beautiful, DRY, and scalable code!