如何在Rails 4中使用concerns

645

默认的Rails 4项目生成器现在在控制器和模型下创建了"concerns"目录。我已经找到了一些关于如何使用路由关注点的解释,但是没有关于控制器或模型的内容。

我很确定这与社区中当前的"DCI趋势"有关,并希望尝试一下。

问题是,我应该如何使用这个功能,是否有惯例来定义命名/类层次结构以使其工作?我如何在模型或控制器中包含一个关注点?

7个回答

631
我自己找到了答案。这其实是一个相当简单但强大的概念,与代码重用有关,就像以下示例一样。基本上,这个想法是为了提取常见和/或上下文特定的代码块,以清理模型并避免它们变得太臃肿和混乱。
例如,我将放置一个众所周知的模式,即可标记模式:
# 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

所以,按照产品示例,您可以将Taggable添加到任何您想要的类中并共享其功能。
这是由DHH很好地解释了:
在Rails 4中,我们将邀请程序员使用concerns与默认的app/models/concerns和app/controllers/concerns目录一起自动成为负载路径的一部分。与ActiveSupport::Concern包装器一起使用,它足以支持这种轻量级的分解机制。

11
DCI(Data, Context, Interaction)处理上下文,使用角色作为标识符将心理模型/用例映射到代码,并且不需要使用包装器(方法直接绑定到对象运行时),所以这与DCI实际上无关。 - ciscoheat
2
@yagooar 即使在运行时包含它也不会使它成为DCI。如果您想查看Ruby DCI示例实现,请查看http://fulloo.info或https://github.com/runefs/Moby上的示例,或者了解如何使用maroon在Ruby中执行DCI以及什么是DCI,请访问http://runefs.com(“什么是DCI”是我最近刚开始的一系列文章)。 - Rune FS
2
@RuneFS和ciscoheat,你们两个都是对的。我重新分析了文章和事实,并且上周末我参加了一个有关Ruby的会议,其中一次演讲是关于DCI的,最终我对其哲学有了更深入的理解。我修改了文本,所以它不再提到DCI。 - yagooar
9
值得一提(并且可能需要包含在示例中)的是,类方法应该被定义在一个特别命名的模块ClassMethods中,并且这个模块也会被ActiveSupport::Concern基类扩展。 - febeling
1
谢谢您提供这个例子,主要是因为我太蠢了,在ClassMethods模块中定义了我的类级别方法,并且使用了self.whatever,但那样行不通=P - Ryan Crews
显示剩余8条评论

388

我一直在研究使用模型关注点来简化臃肿的模型,并减少模型代码冗余。以下是一些解释和例子:

1) 减少模型代码冗余

假设有一个文章模型,一个事件模型和一个评论模型。一篇文章或事件可以有许多评论。而每个评论都属于某篇文章或某个事件。

传统上,这些模型可能长这样:

评论模型:

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

文章模型:

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

事件模型

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

我们可以看到,Event和Article都有相同的一部分代码。通过使用concerns,我们可以将这个共同的部分提取到一个独立的模块Commentable中。

为此,在app/models/concerns中创建一个commentable.rb文件。

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

现在您的模型看起来像这样:

评论模型:

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

文章模型:

class Article < ActiveRecord::Base
  include Commentable
end

事件模型:

class Event < ActiveRecord::Base
  include Commentable
end

2) 精简脂肪模型

考虑一个事件模型。一个事件有很多参与者和评论。

通常情况下,事件模型可能看起来像这样:

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

具有许多关联的模型往往会积累越来越多的代码,变得难以管理。Concerns 提供了一种将臃肿的模块变得更加模块化和易于理解的方法。

可以使用 Concerns 对上述模型进行重构,如下所示:

在 app/models/concerns/event 文件夹中创建 attendable.rb 和 commentable.rb 文件。

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

可评论.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

现在使用Concerns,您的Event模型将被简化为

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

* 在使用concerns时,建议选择基于“领域”而不是“技术”的分组。 基于领域的分组可以是“可评论的”,“可照片化的”,“可出席的”。 技术分组将意味着“ValidationMethods”,“FinderMethods”等


8
关注点只是使用继承、接口或多重继承的一种方式吗?那么从一个共同的基类派生子类有什么问题吗? - Chloe
3
的确,@Chloe,我在某处读到,一个带有“concerns”目录的Rails应用实际上是一个“concern”。 - Ziyan Junaideen
您可以使用“included”块来定义所有的方法和包括:类方法(使用def self.my_class_method),实例方法以及类作用域中的方法调用和指令。无需使用module ClassMethods - A Fader Darkly
1
我对concern的问题在于它们直接向模型添加功能。因此,如果两个concern都实现了add_item,那么你就会遇到麻烦。我记得当一些验证器停止工作时,我曾经认为Rails出了问题,但是有人在concern中实现了any?。我提出了一个不同的解决方案:像另一种语言中的接口一样使用concern。它不是定义功能,而是定义对处理该功能的单独类实例的引用。然后你就有了更小、更整洁的类,每个类只做一件事... - A Fader Darkly
@aaditi_jain:请进行小修改以避免误解。即“在app/models/concerns/event文件夹中创建attendable.rd和commentable.rb文件”--> attendable.rd必须更改为attendable.rb。谢谢。 - Rubyist

102

值得一提的是,许多人认为使用concerns是一个坏主意。

  1. 像这个家伙这样
  2. 还有这个人

一些原因:

  1. 在幕后发生了一些黑魔法——Concern正在修补include方法,有一个完整的依赖处理系统——对于一些微不足道的好老式Ruby混合模式来说,过于复杂了。
  2. 你的类并没有更少的DRY。如果你把50个公共方法塞进各种模块中并包含它们,你的类仍然有50个公共方法,只是你隐藏了那些代码味道,有点像把垃圾放在抽屉里。
  3. 所有这些关注点使代码库实际上更难以导航。
  4. 你确定你团队的所有成员都有同样的理解,什么应该真正替代concern吗?

Concerns是一种容易自掘坑的方式,请小心使用。


2
我知道SO不是讨论这个问题的最佳场所,但还有哪种类型的Ruby mixin可以使你的类保持DRY?除非你只是在为更好的OO设计、服务层或其他我可能忽略的东西辩护,否则你的论点中的理由#1和#2似乎是相互矛盾的。(我并不反对——我建议添加一些替代方案会更有帮助!) - toobulkeh
3
使用https://github.com/AndyObtiva/super_module是一种选择,使用好的老ClassMethods模式是另一种选择。而且,使用更多的对象(如服务)来清晰地分离关注点绝对是正确的方法。 - Dr.Strangelove
4
下投票,因为这不是对问题的回答,而是一个观点。我相信这个观点有其优点,但它不应该成为 StackOverflow 上一个问题的答案。 - Adam
4
@Adam 这是一个有主见的回答。假设有人问如何在Rails中使用全局变量,肯定提到有更好的方法来处理事情(例如Redis.current vs $redis)可能对主题发起者有用的信息?软件开发本质上就是一门充满主见的学科,这是无法避免的。实际上,在stackoverflow上,我经常看到意见作为回答和讨论,哪个答案最好,这是一件好事。 - Dr.Strangelove
2
当然,将它与你对问题的回答一起提及似乎很好。不过,你的回答实际上并没有回答原帖作者的问题。如果你只是想警告某人不能使用 concerns 或全局变量,那么这可以作为一个不错的评论添加到他们的问题中,但这并不是一个好的答案。 - Adam
显示剩余2条评论

57

这篇文章帮助我理解了concerns。

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

# app/models/concerns/shared/schedule.rb
module Shared::Schedule
  extend ActiveSupport::Concern
  ...
end

4
这个答案没有解释任何东西。 - user9903

52

我觉得这里的大部分例子展示了module的威力,而不是ActiveSupport::Concern如何为module增加价值。

示例1:更易读的模块。

因此,如果没有concerns,一个典型的module将是这样的。

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

使用 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

你会发现实例方法、类方法和包含的块会更加整洁。Concerns 将会为你适当地注入它们。这是使用 ActiveSupport::Concern 的一个优点。


示例 2:优雅地处理模块依赖关系。

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
在这个例子中,BarHost真正需要的模块。但由于Bar依赖于Foo,所以Host类必须include Foo(但等一下,为什么Host想要了解Foo呢?能避免吗?)。
因此,Bar在它所到之处都添加了依赖性。并且包含的顺序也非常重要,这给庞大的代码库增加了很多复杂性/依赖关系。
重构后使用了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

现在看起来很简单。

如果你在想为什么不能在Bar模块本身中添加Foo依赖关系?那行不通,因为method_injected_by_foo_to_host_klass必须被注入到一个包括Bar但不是在Bar模块本身上的类中。

来源:Rails ActiveSupport::Concern


顺便提一下,这大致是从文档中复制粘贴的。 - Dave Newton

7

在关注文件filename.rb中

例如,我希望在我的应用程序中,在属性create_by存在时将其值更新为1,而将updated_by更新为0。

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

如果您想在操作中传递参数

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

然后像这样在你的模型中包含:

class Role < ActiveRecord::Base
  include TestConcern
end

-1
在Rails 4中,concerns用于组织模型或控制器之间的共享代码。它们有助于保持代码的组织性并防止重复。以下是如何使用concerns:
创建一个Concern: 导航到app/models/concerns(用于模型)或app/controllers/concerns(用于控制器)。 创建一个以_concern.rb结尾的新的Ruby文件(例如,timestampable_concern.rb)。 将concern定义为一个模块,并使用extend ActiveSupport::Concern。 在模块内部,定义您想要共享的方法或行为。
使用Concern: 在您想要使用concern的模型/控制器中,使用include语句将其包含进来。 被包含的方法将在模型/控制器中可用。 您可以通过链接include语句来包含多个concerns。
注意事项: 为了清晰起见,使用Concern后缀命名concerns。 将共享的concerns放置在适当的concerns目录中。 避免在单个模型/控制器中使用过多的concerns。 Rails 4会自动加载concerns,因此不需要显式的require。
从这里详细阅读Rails 4 Concerns:Rails Concern 4

根据目前的写法,你的回答不够清晰。请编辑以添加更多细节,帮助其他人理解这如何回答所提出的问题。你可以在帮助中心找到关于如何撰写好回答的更多信息。 - Community

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接