Published on

Mastering Rails Concerns: DRY Up Your Code and Enhance Maintainability

Authors

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?

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!