How to use concerns in Rails 4

后端 未结 6 1808
甜味超标
甜味超标 2020-11-22 06:57

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,

相关标签:
6条回答
  • 2020-11-22 07:20

    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

    0 讨论(0)
  • 2020-11-22 07:21

    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.

    0 讨论(0)
  • 2020-11-22 07:25

    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.

    0 讨论(0)
  • 2020-11-22 07:35

    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
    
    0 讨论(0)
  • 2020-11-22 07:38

    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

    0 讨论(0)
  • 2020-11-22 07:38

    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
    
    0 讨论(0)
提交回复
热议问题