Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Rails API: ActiveSupport::ConcernとModule::Concerning(翻訳)

概要

MITライセンスに基づいて翻訳・公開いたします。

以下のRailsガイドを先に読んでおくことをおすすめします。

参考: §9.3 concernを使う -- Rails をはじめよう - Railsガイド

concernsの使い方については以下の記事もおすすめです。

素のRailsは十分に豊かである(翻訳)

🔗 Rails API: ActiveSupport::Concern

典型的なモジュールは、以下のような感じになります。

module M
  def self.included(base)
    base.extend ClassMethods
    base.class_eval do
      scope :disabled, -> { where(disabled: true) }
    end
  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
end

さらに、モジュールの依存関係もきれいに扱えるようになります。モジュールFooと、Fooに依存するモジュールBarがあるとすると、通常は以下のように書きます。

module Foo
  def self.included(base)
    base.class_eval do
      def self.method_injected_by_foo
        ...
      end
    end
  end
end
module Bar
  def self.included(base)
    base.method_injected_by_foo
  end
end
class Host
  include Foo # Barのためにこの依存をincludeする必要がある
  include Bar # BarはHostが本当に必要とするモジュール
end

しかし、Barが依存している対象(つまりFoo)をHostが気にしなければいけないものでしょうか?以下のようにBarFooincludeすれば、これらをHostで直接隠蔽できそうな気がします。

module Bar
  include Foo
  def self.included(base)
    base.method_injected_by_foo
  end
end
class Host
  include Bar
end

残念ながら上のコードは動きません。Fooincludeされるタイミングでは、そのベースはBarモジュールであり、Hostクラスではないからです。ActiveSupport::Concernを使えば、モジュールの依存関係が正しく解決されるようになります。

require "active_support/concern"

module Foo
  extend ActiveSupport::Concern
  included do
    def self.method_injected_by_foo
      ...
    end
  end
end
module Bar
  extend ActiveSupport::Concern
  include Foo

  included do
    self.method_injected_by_foo
  end
end
class Host
  include Bar # 今度はBarがその依存関係を扱うようになるので動く
end

🔗 concernsをprependする

concernsは、includeと同様にprependもサポートしており、対応するprepended doコールバックを提供しています。module ClassMethodsclass_methods doprependされます。

prependは任意の依存関係にも利用されます。

🔗 インスタンスpublicメソッド

🔗 class_methods(&class_methods_module_definition)

渡されたブロックからクラスメソッドを定義します。privateなクラスメソッドも定義可能です。

module Example
  extend ActiveSupport::Concern

  class_methods do
    def foo; puts 'foo'; end

    private
      def bar; puts 'bar'; end
  end
end
class Buzz
  include Example
end
Buzz.foo # => "foo"
Buzz.bar # => private method 'bar' called for Buzz:Class(NoMethodError)

GitHubで表示

🔗 included(base = nil, &block)

渡されたブロックをベースクラスのコンテキストで評価し、ここにクラスマクロを書けるようにします。includedブロックを2つ以上定義すると例外が発生します。

GitHubで表示

🔗 prepended(base = nil, &block)

渡されたブロックをベースクラスのコンテキストで評価し、ここにクラスマクロを書けるようにします。prependedブロックを2つ以上定義すると例外が発生します。

🔗 Rails API: Module::Concerning

🔗 関心(concerns)をひとくちサイズに分離する

大きさが中程度の振る舞いのチャンクを切り出したかったのに、そのチャンクを1個のクラスにmix-inしただけの形になってしまった、ということがよくあります。

PORO(plain old Ruby object)に切り出してカプセル化し、元のオブジェクトと連携(collaboration)したり委譲したりする方法は、多くの場合よい選択です。

しかし、カプセル化する追加のステートが存在しない場合や、親クラスについてDSLスタイルで宣言している場合は、collaboratorを新たに導入すると、シンプルどころかわかりにくくなってしまうことがあります。

こういう場合にありがちなのは、1個のモノリシックなクラスに何もかも押し込めて、おそらく申し訳程度のコメントも書くという「ダメな中で一番ましな方法」に逃げてしまうことです。しかしモジュールを別ファイルに書くと、全体像を把握するためにあちこちを調べるはめになります。

🔗 関心を分離する場合の「嬉しくない」方法

🔗 1: コメントでお茶を濁す

class Todo < ApplicationRecord
  # 他のtodo実装
  # ...

  ## イベントトラッキング
  has_many :events

  before_create :track_creation

  private
    def track_creation
      # ...
    end
end

🔗 2: インラインモジュールを使う

構文のノイズが増える。

class Todo < ApplicationRecord
  # 他のtodo実装
  # ...

  module EventTracking
    extend ActiveSupport::Concern

    included do
      has_many :events
      before_create :track_creation
    end

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

🔗 3: 目障りなmix-inを別ファイルに押し込める

振る舞いのチャンクが「スクロールすれば理解できる」程度の大きさに育ち始めてきたら、諦めて別ファイルに切り出すことになるでしょう。
なお、この程度のサイズなら、動作をひと目で理解しにくくなるとしても、このオーバーヘッド増加はトレードオフとして妥当といえるでしょう。

class Todo < ApplicationRecord
  # 他のtodo実装
  # ...

  include TodoEventTracking
end

🔗 Module#concerningを導入する

関心をひとくちサイズに分割する自然で敷居の低い方法を手に入れるために、mix-inのノイズを減らします。

class Todo < ApplicationRecord
  # 他のtodo実装
  # ...

  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]

小さな一歩ですが、これは以下のような目覚ましい波及効果を生み出します。

  • クラスの振る舞いをひと目で理解できるようになる
  • 関心が分離されることで、ごちゃごちゃしたモノリシックなクラスが整頓される
  • "これは内部で使うものです"という意図を大ざっぱに表現するときにprotectedprivateのモジュール性に頼らずに済む

🔗 concerningprependする

concerningではprepend: true引数がサポートされています。これを指定すると、concernをincludeではなくprependするようになります。

🔗 インスタンスpublicメソッド

🔗 concern(topic, &module_definition)

concernを手軽に定義するショートカットです。

concern :EventTracking do
  ...
end

上は以下と同等です。

module EventTracking
  extend ActiveSupport::Concern

  ...
end

GitHubで表示

🔗 concerning(topic, prepend: false, &block)

新しいconcernを定義してmix-inします。

GitHubで表示

関連記事

Rails: 私の好きなコード(3)”正しく書かれた” concerns(翻訳)

素のRailsは十分に豊かである(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。