如何在Rails 4中使用问题

How to use concerns in Rails 4

默认的Rails4项目生成器现在在控制器和模型下创建目录"关注点"。我发现了一些关于如何使用路由问题的解释,但是没有关于控制器或模型的解释。

我非常肯定这与社区中当前的"DCI趋势"有关,我想尝试一下。

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


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

作为一个例子,我将放一个众所周知的模式,可标记模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 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很好地解释了这一点:

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.


我一直在读关于使用模型关注皮肤脂肪模型和干涸您的模型代码。下面是一个示例说明:

1)烘干型号代码

考虑一个文章模型、一个事件模型和一个评论模型。一篇文章或一个事件有许多评论。注释属于文章或事件。

传统上,模型可能如下所示:

注释模型:

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

文章模型:

1
2
3
4
5
6
7
8
9
10
11
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

事件模型

1
2
3
4
5
6
7
8
9
10
11
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

正如我们可以注意到的,事件和文章都有一段重要的代码。使用关注点,我们可以在单独的可注释模块中提取此公共代码。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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

现在你的模型是这样的:

注释模型:

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

文章模型:

1
2
3
class Article < ActiveRecord::Base
  include Commentable
end

事件模型:

1
2
3
class Event < ActiveRecord::Base
  include Commentable
end

。2)皮肤化脂肪模型。

考虑一个事件模型。一个活动有许多与会者和评论。

通常,事件模型可能如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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

具有许多关联和其他方面的模型趋向于积累越来越多的代码并变得不可管理。关注点提供了一种使脂肪模块更模块化和易于理解的方法。

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

可出席.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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

现在使用关注点,您的事件模型减少到

1
2
3
4
class Event < ActiveRecord::Base
  include Commentable
  include Attendable
end

*尽管使用关注点,但建议使用基于"域"的分组,而不是"技术"分组。基于域的分组类似于"可评论"、"可拍照"、"可出席"。技术分组是指"验证方法"、"finderMethods"等。


值得一提的是,使用关注点被许多人认为是坏主意。

  • 就像这个家伙
  • 这个呢
  • 一些原因:

  • 在幕后发生了一些黑暗的魔法-担心的是修补include方法,有一个完整的依赖处理系统-对于一些微不足道的好的旧Ruby混合模式来说太复杂了。
  • 你的课也同样枯燥。如果你在不同的模块中填充50个公共方法并包含它们,你的类仍然有50个公共方法,只是你隐藏了代码的味道,有点把垃圾放在抽屉里。
  • 代码库实际上很难处理所有这些问题。
  • 你确定你团队的所有成员都有同样的理解,什么才是真正的替代关注点?
  • 担心是一种很容易让你自己陷入困境的方法,小心对待它们。


    这篇文章帮助我理解了关注点。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # app/models/trader.rb
    class Trader
      include Shared::Schedule
    end

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


    我觉得这里的大多数例子都证明了module的威力,而不是ActiveSupport::Concern如何增加module的价值。

    示例1:更可读的模块。

    因此,不必担心典型的module会是怎样的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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重构之后。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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:优雅地处理模块依赖项。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    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真正需要的模块。但是,由于BarFoo有依赖关系,Host类不得不与include Foo有依赖关系(等等,为什么Host想了解Foo呢?可以避免吗?).

    因此,Bar在任何地方都增加了依赖性。而且**包容性的顺序在这里也很重要。**这给庞大的代码库增加了许多复杂性/依赖性。

    ActiveSupport::Concern重构后

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    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


    考虑到这一点,将文件名设为.rb

    例如,我希望在我的应用程序中,属性create_by exists将值更新为1,将值更新为0

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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

    之后,在您的模型中包括如下内容:

    1
    2
    3
    class Role < ActiveRecord::Base
      include TestConcern
    end