634

The default Rails 4 project generator now creates the directory "concerns" under controllers and models. I have found some explanations about how to use routing concerns, but nothing about controllers or models.

I am pretty sure it has to do with the current "DCI trend" in the community and would like to give it a try.

The question is, how am I supposed to use this feature, is there a convention on how to define the naming / class hierarchy in order to make it work? How can I include a concern in a model or controller?

Arslan Ali
  • 16,294
  • 7
  • 51
  • 65
yagooar
  • 15,789
  • 6
  • 17
  • 21

6 Answers6

626

So I found it out by myself. It is actually a pretty simple but powerful concept. It has to do with code reuse as in the example below. Basically, the idea is to extract common and / or context specific chunks of code in order to clean up the models and avoid them getting too fat and messy.

As an example, I'll put one well known pattern, the taggable pattern:

# 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

So following the Product sample, you can add Taggable to any class you desire and share its functionality.

This is pretty well explained by DHH:

In Rails 4, we’re going to invite programmers to use concerns with the default app/models/concerns and app/controllers/concerns directories that are automatically part of the load path. Together with the ActiveSupport::Concern wrapper, it’s just enough support to make this light-weight factoring mechanism shine.

Dinshaw Raje
  • 849
  • 10
  • 31
yagooar
  • 15,789
  • 6
  • 17
  • 21
  • 11
    DCI deals with a Context, uses Roles as identifiers to map a mental model/use case to code, and requires no wrappers to be used (methods are bound directly to the object at runtime) so this has nothing to do with DCI really. – ciscoheat Feb 26 '13 at 05:59
  • @ciscoheat it would be enough not to include the Taggable module in the class but at runtime in a given context. But you're right, this example focuses more on code reusability than on DCI. – yagooar Feb 26 '13 at 08:34
  • 2
    @yagooar even including it at runtime wouldn't make it DCI. If you wish to see a ruby DCI example implementation. Take a look at either http://fulloo.info or the examples at https://github.com/runefs/Moby or for how to use maroon to do DCI in Ruby and what DCI is http://runefs.com (What DCI is. is a series of post I've just started recently) – Rune FS Feb 26 '13 at 10:42
  • 1
    @RuneFS && ciscoheat you were both right. I just analyzed the articles and facts again. And, I went last weekend to a Ruby conference where one talk was about DCI and finally I understood a little bit more about its philosophy. Changed the text so it doesn't mention DCI at all. – yagooar Mar 10 '13 at 18:53
  • 9
    It is worth mentioning (and probably including in an example) that class methods are supposed to be defined in a specially named module ClassMethods, and that this module is extended by the base class be ActiveSupport::Concern, too. – febeling Sep 17 '13 at 07:10
  • 1
    Thank you for this example, mainly b/c I was being dumb and defining my Class level methods inside of the ClassMethods module with self.whatever still, and that doesn't work =P – Ryan Crews Oct 28 '13 at 18:24
  • to others reading this I'd assume the talk @yagooar went to was https://www.youtube.com/watch?v=ZUADinlqHwk curiously enough this question and some related was was part of the inspiration to that talk – Rune FS Mar 12 '14 at 13:48
  • Theres a good example of using concerns with controllers here: http://elegantbrew.tumblr.com/post/70990048275/controller-concerns-in-rails-4 – Schrute Oct 21 '14 at 05:56
  • Can I use it in Serializes ? – sunil Mar 19 '15 at 12:30
  • 1
    The bit that I don't fully understand is why there are two places for class-level stuff: (1) ClassMethods and (2) included do..end. – Trejkaz Aug 02 '16 at 00:09
  • This is an amazing concept! Thanks for explaining. I find the word "concern" pretty ugly and counter-intuitive. "common" would work better with my brain. But whatever. Let's embrace it! – Riccardo Apr 21 '17 at 11:30
  • Since Rails 4.2 we have [`#class_methods`](http://api.rubyonrails.org/v4.2.0/classes/ActiveSupport/Concern.html#method-i-class_methods) in addition to the hard-coded `ClassMethods`. – Franklin Yu Jun 23 '17 at 03:39
  • This is giving me a ```NoMethodError (undefined method `tags_string=' for nil:NilClass):``` with the line: `product.tags_string = "test, adfd 2 343, 3g4g4, lol"` – CyberMew May 30 '18 at 06:58
384

I have been reading about using model concerns to skin-nize fat models as well as DRY up your model codes. Here is an explanation with examples:

1) DRYing up model codes

Consider a Article model, a Event model and a Comment model. An article or an event has many comments. A comment belongs to either Article or Event.

Traditionally, the models may look like this:

Comment Model:

class Comment < ActiveRecord::Base
  belongs_to :commentable, polymorphic: true
end

Article Model:

class Article < ActiveRecord::Base
  has_many :comments, as: :commentable 

  def find_first_comment
    comments.first(created_at DESC)
  end

  def self.least_commented
   #return the article with least number of comments
  end
end

Event Model

class Event < ActiveRecord::Base
  has_many :comments, as: :commentable 

  def find_first_comment
    comments.first(created_at DESC)
  end

  def self.least_commented
   #returns the event with least number of comments
  end
end

As we can notice, there is a significant piece of code common to both Event and Article. Using concerns we can extract this common code in a separate module Commentable.

For this create a commentable.rb file in app/models/concerns.

module Commentable
  extend ActiveSupport::Concern

  included do
    has_many :comments, as: :commentable
  end

  # for the given article/event returns the first comment
  def find_first_comment
    comments.first(created_at DESC)
  end

  module ClassMethods
    def least_commented
      #returns the article/event which has the least number of comments
    end
  end
end

And now your models look like this :

Comment Model:

class Comment < ActiveRecord::Base
  belongs_to :commentable, polymorphic: true
end

Article Model:

class Article < ActiveRecord::Base
  include Commentable
end

Event Model:

class Event < ActiveRecord::Base
  include Commentable
end

2) Skin-nizing Fat Models.

Consider a Event model. A event has many attenders and comments.

Typically, the event model might look like this

class Event < ActiveRecord::Base   
  has_many :comments
  has_many :attenders


  def find_first_comment
    # for the given article/event returns the first comment
  end

  def find_comments_with_word(word)
    # for the given event returns an array of comments which contain the given word
  end 

  def self.least_commented
    # finds the event which has the least number of comments
  end

  def self.most_attended
    # returns the event with most number of attendes
  end

  def has_attendee(attendee_id)
    # returns true if the event has the mentioned attendee
  end
end

Models with many associations and otherwise have tendency to accumulate more and more code and become unmanageable. Concerns provide a way to skin-nize fat modules making them more modularized and easy to understand.

The above model can be refactored using concerns as below: Create a attendable.rb and commentable.rb file in app/models/concerns/event folder

attendable.rb

module Attendable
  extend ActiveSupport::Concern

  included do 
    has_many :attenders
  end

  def has_attender(attender_id)
    # returns true if the event has the mentioned attendee
  end

  module ClassMethods
    def most_attended
      # returns the event with most number of attendes
    end
  end
end

commentable.rb

module Commentable
  extend ActiveSupport::Concern

  included do 
    has_many :comments
  end

  def find_first_comment
    # for the given article/event returns the first comment
  end

  def find_comments_with_word(word)
    # for the given event returns an array of comments which contain the given word
  end

  module ClassMethods
    def least_commented
      # finds the event which has the least number of comments
    end
  end
end

And now using Concerns, your Event model reduces to

class Event < ActiveRecord::Base
  include Commentable
  include Attendable
end

* While using concerns its advisable to go for 'domain' based grouping rather than 'technical' grouping. Domain Based grouping is like 'Commentable', 'Photoable', 'Attendable'. Technical grouping will mean 'ValidationMethods', 'FinderMethods' etc

Aaditi Jain
  • 6,651
  • 2
  • 21
  • 25
  • 6
    So Concerns are just a way to use inheritance or interfaces or multiple inheritance? What's wrong with creating a common base class and subclassing from that common base class? – Chloe Sep 06 '15 at 00:14
  • 3
    Indeed @Chloe, I some where red, a Rails app with a 'concerns' directory is actually a 'concern'... – Ziyan Junaideen Nov 06 '15 at 17:02
  • You can use the 'included' block to define all your methods and includes: class methods (with ```def self.my_class_method```), instance methods and method calls and directives in the class scope. No need for ```module ClassMethods``` – A Fader Darkly Nov 26 '15 at 15:28
  • 1
    The problem I have with concerns is that they add functionality directly to the model. So if two concerns both implement ```add_item```, for example, you're screwed. I remember thinking Rails was broken when some validators stopped working, but someone had implemented ```any?``` in a concern. I propose a different solution: use the concern like an interface in a different language. Instead of defining the functionality, it defines the reference to a separate class instance that handles that functionality. Then you have smaller, neater classes that do one thing... – A Fader Darkly Nov 26 '15 at 15:33
  • @aaditi_jain : Please correct small change to avoid misconception. ie "Create a attendable.rd and commentable.rb file in app/models/concerns/event folder" --> attendable.rd has to be attendable.rb Thanks – Rubyist Dec 06 '16 at 11:43
99

It's worth to mention that using concerns is considered bad idea by many.

  1. like this guy
  2. and this one

Some reasons:

  1. There is some dark magic happening behind the scenes - Concern is patching include method, there is a whole dependency handling system - way too much complexity for something that's trivial good old Ruby mixin pattern.
  2. Your classes are no less dry. If you stuff 50 public methods in various modules and include them, your class still has 50 public methods, it's just that you hide that code smell, sort of put your garbage in the drawers.
  3. Codebase is actually harder to navigate with all those concerns around.
  4. Are you sure all members of your team have same understanding what should really substitute concern?

Concerns are easy way to shoot yourself in the leg, be careful with them.

rii
  • 1,454
  • 1
  • 16
  • 20
Dr.Strangelove
  • 1,444
  • 10
  • 12
  • 1
    I know SO is not the best place for this discussion, but what other type of Ruby mixin keeps your classes dry? It seems like reasons #1 and #2 in your arguments are counter, unless you're just making the case for better OO design, the services layer, or something else I'm missing? (I don't disagree -- I'm suggesting adding alternatives helps!) – toobulkeh Jul 22 '15 at 16:56
  • 2
    Using https://github.com/AndyObtiva/super_module is one option, using good old ClassMethods patterns is another one. And using more objects(like services) to cleanly separate concerns is definitely the way to go. – Dr.Strangelove Aug 04 '15 at 13:26
  • So why does it exist ? Why was it included in Rails core ? – Laurent Nov 17 '16 at 16:32
  • actually the first linked article seem to present concerns as good idea :) – gorn Jan 02 '18 at 01:27
  • 4
    Downvoting because this is not an answer to the question. It's an opinion. It's an opinion that I'm sure has it's merits but it shouldn't be an answer to a question on StackOverflow. – Adam Jul 25 '18 at 00:06
  • 3
    @Adam It's an opinionated answer. Imagine someone would ask how to use global variables in rails, surely mention that there are better ways to do things (i.e. Redis.current vs $redis) could be useful info for topic starter? Software development is inherently an opinionated discipline, there is no getting around it. In fact, I see opinions as answers and discussions which answer is the best all the time on stackoverflow, and it is a good thing – Dr.Strangelove Jul 26 '18 at 04:29
  • 2
    Sure, mentioning it along with your _answer_ to the question seems fine. Nothing in your answer actually answers the OP's question though. If all you wish to do is warn someone why they shouldn't use concerns or global variables then that would make for a good comment that you could add to their question, but it doesn't really make for a good answer. – Adam Jul 26 '18 at 18:50
56

This post helped me understand concerns.

# app/models/trader.rb
class Trader
  include Shared::Schedule
end

# app/models/concerns/shared/schedule.rb
module Shared::Schedule
  extend ActiveSupport::Concern
  ...
end
davidrac
  • 10,318
  • 3
  • 35
  • 70
aminhotob
  • 1,006
  • 14
  • 16
  • 1
    Internet Archive version: https://web.archive.org/web/20130712014326/http://blog.andywaite.com/2012/12/23/exploring-concerns-for-rails-4/ – MZB Jun 04 '15 at 21:16
  • 1
    this answer doesn't explain anything. –  Oct 09 '18 at 18:58
50

I felt most of the examples here demonstrated the power of module rather than how ActiveSupport::Concern adds value to module.

Example 1: More readable modules.

So without concerns this how a typical module will be.

module M
  def self.included(base)
    base.extend ClassMethods
    base.class_eval do
      scope :disabled, -> { where(disabled: true) }
    end
  end

  def instance_method
    ...
  end

  module ClassMethods
    ...
  end
end

After refactoring with ActiveSupport::Concern.

require 'active_support/concern'

module M
  extend ActiveSupport::Concern

  included do
    scope :disabled, -> { where(disabled: true) }
  end

  class_methods do
    ...
  end

  def instance_method
    ...
  end
end

You see instance methods, class methods and included block are less messy. Concerns will inject them appropriately for you. That's one advantage of using ActiveSupport::Concern.


Example 2: Handle module dependencies gracefully.

module Foo
  def self.included(base)
    base.class_eval do
      def self.method_injected_by_foo_to_host_klass
        ...
      end
    end
  end
end

module Bar
  def self.included(base)
    base.method_injected_by_foo_to_host_klass
  end
end

class Host
  include Foo # We need to include this dependency for Bar
  include Bar # Bar is the module that Host really needs
end

In this example Bar is the module that Host really needs. But since Bar has dependency with Foo the Host class have to include Foo (but wait why does Host want to know about Foo? Can it be avoided?).

So Bar adds dependency everywhere it goes. And order of inclusion also matters here. This adds lot of complexity/dependency to huge code base.

After refactoring with ActiveSupport::Concern

require 'active_support/concern'

module Foo
  extend ActiveSupport::Concern
  included do
    def self.method_injected_by_foo_to_host_klass
      ...
    end
  end
end

module Bar
  extend ActiveSupport::Concern
  include Foo

  included do
    self.method_injected_by_foo_to_host_klass
  end
end

class Host
  include Bar # It works, now Bar takes care of its dependencies
end

Now it looks simple.

If you are thinking why can't we add Foo dependency in Bar module itself? That won't work since method_injected_by_foo_to_host_klass have to be injected in a class that's including Bar not on Bar module itself.

Source: Rails ActiveSupport::Concern

bad_coder
  • 5,829
  • 13
  • 26
  • 41
Siva
  • 6,990
  • 4
  • 41
  • 53
7

In concerns make file filename.rb

For example I want in my application where attribute create_by exist update there value by 1, and 0 for updated_by

module TestConcern 
  extend ActiveSupport::Concern

  def checkattributes   
    if self.has_attribute?(:created_by)
      self.update_attributes(created_by: 1)
    end
    if self.has_attribute?(:updated_by)
      self.update_attributes(updated_by: 0)
    end
  end

end

If you want to pass arguments in action

included do
   before_action only: [:create] do
     blaablaa(options)
   end
end

after that include in your model like this:

class Role < ActiveRecord::Base
  include TestConcern
end
Sajjad Murtaza
  • 1,266
  • 2
  • 15
  • 26