Conquering Code Clutter - A Guide to Rails Concerns

railsJanuary 03, 2024Dotby Alkesh Ghorpade

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 the include 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 a Searchable concern with basic functionality like search Post based on text and description.

    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 a search 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 like Comment. If you keep adding Post model-related methods to the Searchable 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 of Validatable.

  • 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!

Closing Remark

Could your team use some help with topics like this and others covered by ShakaCode's blog and open source? We specialize in optimizing Rails applications, especially those with advanced JavaScript frontends, like React. We can also help you optimize your CI processes with lower costs and faster, more reliable tests. Scraping web data and lowering infrastructure costs are two other areas of specialization. Feel free to reach out to ShakaCode's CEO, Justin Gordon, at justin@shakacode.com or schedule an appointment to discuss how ShakaCode can help your project!
Are you looking for a software development partner who can
develop modern, high-performance web apps and sites?
See what we've doneArrow right