Concerns – Rails's take on... well... separation of concerns – When might I want to use concerns?



Concerns – Rails's take on... well... separation of concerns – When might I want to use concerns?

0 0


concerns


On Github mdms-scott / concerns

Concerns

Rails's take on... well... separation of concerns

Separation of concerns?

"In computer science, separation of concerns (SoC) is a design principle for separating a computer program into distinct sections, such that each section addresses a separate concern. A concern is a set of information that affects the code of a computer program. A concern can be as general as the details of the hardware the code is being optimized for, or as specific as the name of a class to instantiate. A program that embodies SoC well is called a modular program... The value of separation of concerns is simplifying development and maintenance of computer programs. When concerns are well-separated, individual sections can be developed and updated independently. Of special value is the ability to later improve or modify one section of code without having to know the details of other sections, and without having to make corresponding changes to those sections."

When might I want to use concerns?

  • Tagging things
  • Adding comments to things
  • Adding status to things
  • Scoping your objects

So you want to tag some models?

Basic tagging

# app/models/product.rb
class Product
  include Taggable

  ...
end

# app/models/concerns/taggable.rb
# notice that the file name has to match the module name 
# (applying Rails conventions for autoloading)
module Taggable
  extend ActiveSupport::Concern

  included do
    has_many :taggings, as: :taggable
    has_many :tags, through: :taggings

    class_attribute :tag_limit
  end

  def tags_string
    tags.map(&:name).join(', ')
  end

  def tags_string=(tag_string)
    tag_names = tag_string.to_s.split(', ')

    tag_names.each do |tag_name|
      tags.build(name: tag_name)
    end
  end

  # methods defined here are going to extend the class, not the instance of it
  module ClassMethods

    def tag_limit(value)
      self.tag_limit_value = value
    end

  end

end

Courtesy of stack overflow.

So then we can do...

product = Product.new
product.tags_string = "green, large, shirt"
product.tags_string
 => "green, large, shirt"
product.tags
 => [{name: "green"}, {name: "large"}, {name: "shirt"}]

Product.tag_limit(5)

But Matt, what if I want to scope

multiple models by date?

Well lets take a look:

module DateScopes
  extend ActiveSupport::Concern

  module ClassMethods

    def include_date_scopes

      define_singleton_method :"between" do |start_datetime, stop_datetime|
        where ['created_at >= AND created_at <', start_datetime, stop_datetime]
      end
      
      define_singleton_method :"on_or_before_date" do |date|
        where ['created_at <= ?', date]
      end
    end
  end
end

And then...

class Article << ActiveRecord::Base
  include DateScopes

  include_date_scopes

  #...
end

And then...

Article.between((Time.now - 7.days), Time.now)
 => #<ActiveRecord::Relation [...]>
Article.between(Time.now, (Time.now + 7.days))
 => #<ActiveRecord::Relation []>

Article.on_or_before(Time.now - 7.years)
 => #<ActiveRecord::Relation []>
Article.between(Time.now)
 => #<ActiveRecord::Relation [...]>

But Matt, why is there a concerns folder in the controllers directory?

You can do many things with controller concerns:

  • Authentication
  • Error handling
  • Shared actions

Let's take a look at error handling

module RescueExceptions
  extend ActiveSupport::Concern

  included do
    rescue_from ::ActionView::MissingTemplate, :with => :handle_missing_page
    rescue_from ::ActiveRecord::RecordNotFound, :with => :handle_missing_record
  end

  protected

  def handle_missing_page(exception)
    logger.error("#{exception.class.name}: #{exception.message} #{exception.backtrace.join("\n")}")
    redirect_to your_404_path
  end

  def handle_missing_record(exception)
    logger.error("#{exception.class.name}: #{exception.message} #{exception.backtrace.join("\n")}")
    redirect_to your_missing_record_page
  end

end