How to use concerns in Rails 4
Asked Answered
B

6

646

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?

Briquette answered 26/1, 2013 at 21:36 Comment(0)
B
631

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.

Briquette answered 25/2, 2013 at 22:50 Comment(13)
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.Supposition
@Supposition 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.Briquette
@Briquette 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 fulloo.info or the examples at github.com/runefs/Moby or for how to use maroon to do DCI in Ruby and what DCI is runefs.com (What DCI is. is a series of post I've just started recently)Done
@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.Briquette
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.Begotten
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 =PPicker
to others reading this I'd assume the talk @Briquette went to was youtube.com/watch?v=ZUADinlqHwk curiously enough this question and some related was was part of the inspiration to that talkDone
Theres a good example of using concerns with controllers here: elegantbrew.tumblr.com/post/70990048275/…Uralite
Can I use it in Serializes ?Introductory
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.Slover
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!Katharinakatharine
Since Rails 4.2 we have #class_methods in addition to the hard-coded ClassMethods.Abbieabbot
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"Biota
T
388

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

Thorianite answered 15/9, 2014 at 22:50 Comment(5)
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?Mcneill
Indeed @Chloe, I some where red, a Rails app with a 'concerns' directory is actually a 'concern'...Twofold
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 ClassMethodsOutstation
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...Outstation
@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 ThanksMedley
G
102

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.

Grosso answered 23/3, 2015 at 14:11 Comment(7)
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!)Cynthia
Using 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.Grosso
So why does it exist ? Why was it included in Rails core ?Appositive
actually the first linked article seem to present concerns as good idea :)Supranational
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.Aylward
@Aylward 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 thingGrosso
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.Aylward
K
57

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
Knickknack answered 17/3, 2013 at 19:20 Comment(1)
this answer doesn't explain anything.Gautier
P
52

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

Popover answered 3/12, 2015 at 9:11 Comment(1)
FWIW this is roughly copy-paste from the docs.Lise
H
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
Habituate answered 15/1, 2015 at 8:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.