跳至内容 跳至搜索

精巧地分离关注点

我们经常发现自己有一个中等大小的行为块,我们希望将其提取出来,但只混入一个类。

提取一个普通的 Ruby 对象来封装它并与原始对象协作或委托通常是一个不错的选择,但当没有额外的状态需要封装时,或者当我们正在对父类进行 DSL 式声明时,引入新的协作者会使事情变得复杂而不是简化。

通常的路线是将所有内容都塞进一个单体类中,可能还会附带一个注释,作为最不坏的选择。使用独立文件中的模块意味着需要进行繁琐的筛选才能获得全局视图。

分离小关注点的令人不满意的做法

使用注释:

class Todo < ApplicationRecord
  # Other todo implementation
  # ...

  ## Event tracking
  has_many :events

  before_create :track_creation

  private
    def track_creation
      # ...
    end
end

使用内联模块:

冗余的语法。

class Todo < ApplicationRecord
  # Other todo implementation
  # ...

  module EventTracking
    extend ActiveSupport::Concern

    included do
      has_many :events
      before_create :track_creation
    end

    private
      def track_creation
        # ...
      end
  end
  include EventTracking
end

混入噪声被迁移到自己的文件中:

一旦我们的行为块开始超出滚动理解的界限,我们就会屈服,将其移动到一个单独的文件中。在这个规模下,增加的开销可能是一个合理的权衡,即使它降低了我们一目了然地了解事物运作方式的感知。

class Todo < ApplicationRecord
  # Other todo implementation
  # ...

  include TodoEventTracking
end

引入 Module#concerning

通过静默混入噪声,我们得以一种自然、低开销的方式来分离精巧的关注点。

class Todo < ApplicationRecord
  # Other todo implementation
  # ...

  concerning :EventTracking do
    included do
      has_many :events
      before_create :track_creation
    end

    private
      def track_creation
        # ...
      end
  end
end

Todo.ancestors
# => [Todo, Todo::EventTracking, ApplicationRecord, Object]

这一小步带来了一些奇妙的连锁效应。我们可以

  • 一目了然地理解我们类的行为,

  • 通过分离它们的关注点来清理单体的杂物抽屉类,并且

  • 停止依赖 protected/private 来实现粗糙的“这是内部东西”的模块化。

预置 concerning

concerning 支持一个 prepend: true 参数,该参数将 prepend 关注点而不是使用 include 来进行。

方法
C

实例公共方法

concern(topic, &module_definition)

一个低样板的快捷方式来定义一个关注点。

concern :EventTracking do
  ...
end

等同于

module EventTracking
  extend ActiveSupport::Concern

  ...
end
# File activesupport/lib/active_support/core_ext/module/concerning.rb, line 132
def concern(topic, &module_definition)
  const_set topic, Module.new {
    extend ::ActiveSupport::Concern
    module_eval(&module_definition)
  }
end

concerning(topic, prepend: false, &block)

定义一个新关注点并将其混入。

# File activesupport/lib/active_support/core_ext/module/concerning.rb, line 114
def concerning(topic, prepend: false, &block)
  method = prepend ? :prepend : :include
  __send__(method, concern(topic, &block))
end