DRYing up Ruby code with Scopes

Many times I find myself or fellow team members in need of DRYing up their code or encapsulating logic to avoid redundancy but often times the solutions proposed introduce more complexity than what it's worth.

In the quest for cleaner and more maintainable code, we often turn to techniques like .send and super(), hoping to encapsulate logic and reduce redundancy. However, as our applications grow in size and complexity, these solutions can sometimes do more harm than good. The use of such techniques can increase cognitive load, making it challenging for developers to navigate and understand the codebase effectively.

Consider the case of super(), which might refer to a method potentially buried deep within Ruby's inheritance hierarchy. Imagine the mental gymnastics required to trace the execution flow back to its origin, all while trying to stay focused on the task at hand. It's no wonder that developers can find themselves lost in a sea of complexity.

Over time I found a good solution to that problem might be surprisingly simple: using Rails' scopes. For those of you who aren't familiar with scopes they encapsulate commonly used queries so they're easily accessible from a Model's instance. For example, say we have a Products model and we need to fetch the products that are available. A scope to achieve that might look like the following:

class Product < ApplicationRecord 
  scope :available, -> { where(available: true) } 
end

Hence, if you need to chain it with other queries the code becomes super simple and highly legible:

Product.available.where(category: 'Electronics')

Compare that approach with the following example that uses inheritance to achieve the same functionality:

# Base model class
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true

  def self.available
    where(available: true)
  end
end

# Subclass inheriting from ApplicationRecord
class Product < ApplicationRecord
  def self.category(category)
    where(category: category)
  end
end

Those are at least two classes already and significantly more code to achieve the same functionality. So to summarize, a few advantages of scopes over super() and send are as follows:

  1. Simplicity and Clarity: Scopes are defined directly within the model, serving as a single source of truth for data retrieval logic. This simplicity makes code more readable and easier to maintain over time.

  2. Reduced Complexity: Scopes help avoid the introduction of unnecessary complexity, particularly in files that are already complex. By encapsulating logic within the model, scopes keep code clean and focused on its primary purpose.

  3. Promotion of Composition over Inheritance: By embracing scopes, you set a precedent for favoring composition over inheritance in your codebase. This approach aligns with best practices in object-oriented design and encourages modular, reusable code.

In conclusion, when it comes to refactoring code in Ruby on Rails applications, simplicity is key. By leveraging Rails' scopes, we can streamline code, reduce complexity, and foster a more maintainable codebase.

Thank you for reading and I hope you've enjoyed it,
Teo.

Did you find this article valuable?

Support Teogenes Moura by becoming a sponsor. Any amount is appreciated!