如何在Rails 4中使用关注点


627

现在,默认的Rails 4项目生成器将在控制器和模型下创建目录“问题”。我找到了一些有关如何使用路由问题的解释,但是没有关于控制器或模型的解释。

我很确定这与社区中当前的“ DCI趋势”有关,并且想尝试一下。

问题是,我应该如何使用此功能,是否有关于如何定义命名/类层次结构以使其起作用的约定?如何在模型或控制器中包含问题?

Answers:


617

所以我自己发现了。这实际上是一个非常简单但功能强大的概念。如以下示例所示,它与代码重用有关。基本上,这个想法是提取公共的和/或上下文特定的代码块,以清理模型并避免它们变得过于繁琐和混乱。

例如,我将放置一个众所周知的模式,即可标记模式:

# 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中,我们将邀请程序员对默认的app / models / concerns和app / controllers / concerns目录使用关注点,这些目录自动成为加载路径的一部分。与ActiveSupport :: Concern包装器一起使用,它足以使此轻量级分解机制发挥作用。


11
DCI处理上下文,使用角色作为标识符将思维模型/用例映射到代码,并且不需要使用包装器(方法在运行时直接绑定到对象),因此这与DCI无关。
ciscoheat

2
@yagooar甚至在运行时包括它也不会使其成为DCI。如果您希望看到ruby DCI示例实现。看看要么fulloo.info或例子github.com/runefs/Moby或如何使用栗色做DCI在Ruby中,什么是DCI runefs.com(DCI什么是。是一系列文章中,我我-我的才刚刚开始)
符文FS

1
@RuneFS && ciscoheat你们都是正确的。我只是再次分析了文章和事实。而且,我上周末参加了Ruby会议,其中一次是关于DCI的演讲,最后我对它的理念有了更多的了解。更改了文本,因此根本没有提到DCI。
yagooar

9
值得一提的是(可能包括在示例中)类方法应该在特别命名的模块ClassMethods中定义,并且该模块也由基类ActiveSupport :: Concern扩展。
2013年

1
谢谢您的这个示例,主要是b / c,我很笨,用self.all仍然在ClassMethods模块中定义了我的Class级方法。无论如何,那还是行不通的。= P
Ryan Crews

378

我一直在阅读有关使用模型关注点对脂肪模型进行皮肤化以及干燥模型代码的内容。以下是带有示例的说明:

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

我们可以注意到,事件和文章都有大量的通用代码。使用关注点,我们可以在一个单独的模块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

具有许多关联的模型,否则具有积累越来越多的代码并变得难以管理的趋势。关注提供了一种使脂肪模块皮肤化的方法,使它们更加模块化且易于理解。

可以使用以下关注点来重构上述模型:创建一个attendable.rbcommentable.rb在app / models / concerns / event文件夹中文件。

Attenable.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

现在,使用关注点,您的事件模型可以简化为

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

*在使用关注点时,建议您进行基于“域”的分组而不是“技术”分组。基于域的分组就像“ Commentable”,“ Photoable”,“ Attendable”。技术分组将表示“ ValidationMethods”,“ FinderMethods”等


6
因此,关注只是使用继承或接口还是多重继承的一种方式?创建公共基类并从该公共基类中继承子类有什么问题?
Chloe 2015年

3
确实,@ Chloe,在某些地方红色,带有“关注”目录的Rails应用实际上是“关注” ...
Ziyan Junaideen

您可以使用“ included”块来定义所有方法,包括:类方法(带有def self.my_class_method),实例方法以及类范围内的方法调用和指令。无需module ClassMethods
Fader Darkly,2015年

1
我担心的问题是,它们直接向模型添加了功能。因此add_item,例如,如果两个问题都实现了,那就太麻烦了。我记得当某些验证器停止工作时,我以为Rails坏了,但是有人any?担心地实施了。我提出了一个不同的解决方案:像使用其他语言的界面一样使用关注点。它没有定义功能,而是定义了对处理该功能的单独类实例的引用。然后,您会有较小的,更整洁的类来做一件事……
黑暗的Fader,2015年

@aaditi_jain:请更正零钱,以免造成误解。即“在app / models / concerns / event文件夹中创建Attenable.rd和commentable.rb文件”-> Attenable.rd必须为Attenable.rb,谢谢
Rubyist

96

值得一提的是,使用顾虑被许多人视为坏主意。

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

原因如下:

  1. 幕后发生了一些不可思议的事情-关注补丁程序include,有一个完整的依赖项处理系统-对于琐碎的旧Ruby mixin模式而言,这太复杂了。
  2. 您的课程也不少。如果您在各种模块中填充50个公共方法并将其包括在内,则您的类仍然有50个公共方法,只是您隐藏了代码味道,有点将您的垃圾放在抽屉里了。
  3. 实际上,代码库很难解决所有这些问题。
  4. 您确定团队中的所有成员都具有相同的理解,该用什么真正替代关注?

担心是射杀自己腿部的简单方法,请谨慎对待。


1
我知道SO并不是进行此讨论的最佳场所,但是还有什么其他类型的Ruby mixin使您的课程枯燥?似乎您的论点中的#1和#2理由相反,除非您只是为了更好地进行OO设计,服务层或其他我所缺少的理由?(我不同意-我建议添加替代方法有帮助!)
toobulkeh 2015年

2
使用github.com/AndyObtiva/super_module是一种选择,使用旧的良好ClassMethods模式是另一种选择。使用更多对象(例如服务)来明确分离关注点绝对是正确的方法。
Strangelove博士,2015年

4
拒绝投票,因为这不是问题的答案。这是一个意见。我敢肯定这是一种优点,但这不应该是对StackOverflow问题的答案。
亚当

2
@Adam这是一个自以为是的答案。试想一下,有人会问如何在rails中使用全局变量,一定会提到有更好的做事方法(例如Redis.current与$ redis)可能对主题入门有用吗?软件开发本质上是一门自以为是的学科,没有绕过它的地方。实际上,我认为观点是答案和讨论,这些答案一直以来都是stackoverflow上最好的,这是一件好事
Dr.Strangelove

2
当然,将其与您对问题的答案一起提及似乎很好。您的答案中没有任何内容实际上可以回答OP的问题。如果您只想警告某人,为什么他们不应该使用关注点或全局变量,那么您可以在他们的问题中添加一个很好的注释,但这并不能真正为您提供一个很好的答案。
亚当


46

我觉得这里的大多数例子都展示了力量module而非ActiveSupport::Concern增值的力量module

范例1:更具可读性的模块。

因此,不用担心这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

您会看到实例方法,类方法和所包含的块变得不太混乱。顾虑将为您适当注入。这是使用的优势之一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有依赖FooHost类必须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

现在看起来很简单。

如果您在考虑为什么我们不能FooBar模块本身中添加依赖项?那是行不通的,因为method_injected_by_foo_to_host_klass必须将其注入一个Bar不包含在Bar模块本身中的类中。

来源: Rails ActiveSupport ::关注


感谢那。我开始怀疑他们的优势是什么……
Hari Karam Singh

FWIW,这大致是从docs复制粘贴。
戴夫牛顿

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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.