🔗 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
が気にしなければいけないものでしょうか?以下のようにBar
でFoo
をinclude
すれば、これらをHost
で直接隠蔽できそうな気がします。
module Bar
include Foo
def self.included(base)
base.method_injected_by_foo
end
end
class Host
include Bar
end
残念ながら上のコードは動きません。Foo
がinclude
されるタイミングでは、そのベースは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 ClassMethods
やclass_methods do
もprepend
されます。
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)
🔗 included(base = nil, &block)
渡されたブロックをベースクラスのコンテキストで評価し、ここにクラスマクロを書けるようにします。included
ブロックを2つ以上定義すると例外が発生します。
🔗 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]
小さな一歩ですが、これは以下のような目覚ましい波及効果を生み出します。
- クラスの振る舞いをひと目で理解できるようになる
- 関心が分離されることで、ごちゃごちゃしたモノリシックなクラスが整頓される
- "これは内部で使うものです"という意図を大ざっぱに表現するときに
protected
やprivate
のモジュール性に頼らずに済む
🔗 concerning
をprepend
する
concerning
ではprepend: true
引数がサポートされています。これを指定すると、concernをinclude
ではなくprepend
するようになります。
🔗 インスタンスpublicメソッド
🔗 concern(topic, &module_definition)
concernを手軽に定義するショートカットです。
concern :EventTracking do
...
end
上は以下と同等です。
module EventTracking
extend ActiveSupport::Concern
...
end
🔗 concerning(topic, prepend: false, &block)
新しいconcernを定義してmix-inします。
概要
MITライセンスに基づいて翻訳・公開いたします。
ActiveSupport::Concern
-- 更新日: 2021/07/30(18707ab)Module::Concerning
-- 更新日: 2020/11/17(888d8c7)以下のRailsガイドを先に読んでおくことをおすすめします。
参考: §9.3 concernを使う -- Rails をはじめよう - Railsガイド
concernsの使い方については以下の記事もおすすめです。