Concern/ActiveSupport::Concern の使い方
Concern とは?
ModelやControllerを構成する一部の概念や機能を実装するためのモジュールのことを指します。特定の概念や機能を有するロジックをModelやControllerとは分離させ実装することができます。また、複数のModelやControllerで再利用することができます。
Concernを使用するには、主にActiveSupport::Concernというライブラリを用いることが通例となっています。
ActiveSupport::Concern とは?
Concernの実装を容易にしてくれるライブラリのことです。
ActiveSupport::Concernを extend することで、クラス定義を容易にする class_methods メソッドやブロック渡しができる included メソッドなどを使用できるようになります。
Concern の使用タイミング
Concern を利用する前に、特定のロジックを値オブジェクトやサービスオブジェクトとして実装することを検討するべきです。
同様に、コントローラのConcernを利用する前には、そのロジックをモデルやフォームオブジェクトに実装することを考えるべきです。
Concernを利用すべきなのは、これらのオブジェクトに実装することが適さない場合のみです。
次にConcernの使用例について紹介します。
使用例① 通常の Concern
Concernを使用するときは、まず Concern (モジュール)を作成し、ActiveSupport::Concern を extend します。
そして、作成した Concern (モジュール)include したいクラスへ include することでそのクラス内で使用することができるようになります。
app/models/concerns/searchable.rb
module Searchable
extend ActiveSupport::Concern
def search(query)
where("name LIKE ?", "%#{query}%")
end
end
app/controllers/users_controller.rb
class UsersController < ApplicationController
include Searchable
def index
if params[:query].present?
@users = search(params[:query])
else
@users = User.all
end
end
end
上記の例では、search
メソッド内でwhere
メソッドを使用して、name
カラムが指定されたクエリに一致するデータを検索しています。
まず、UsersController
のindex
アクション内でsearch
メソッドを呼び出しています。リクエストパラメータquery
が指定されている場合、User.search(params[:query])
によって指定されたクエリに一致するユーザーを検索します。指定されていない場合は、全てのユーザーを取得します。
このようにすることで、UsersController
内で検索機能を再利用することができます。例えば、別のコントローラーでも同じSearchable
モジュールを include すれば、同様の検索機能を利用することができます。
使用例② ブロック渡しする included メソッド
ActiveSupport::Concern をextendすると、included メソッドを使用することができるようになります。
included メソッドは、include した際に呼び出されるコールバックメソッドです。
つまり、クラスがあるモジュールを include
すると、そのモジュール内で定義された included
メソッドを自動的に呼び出すことができます。
また、ActiveSupport::Concern では included メソッドは下記のように定義されています。
def included(base = nil, &block)
if base.nil?
if instance_variable_defined?(:@_included_block)
if @_included_block.source_location != block.source_location
raise MultipleIncludedBlocks
end
else
@_included_block = block
end
else
super
end
end
included メソッドがどのようなことを行なっているのか解説します。
まず、included メソッドは、2つの引数を取ります。
base
はクラスオブジェクトまたはモジュールオブジェクトを表す変数であり、block
はブロックオブジェクトを表す引数です。
このメソッドの主な目的は、base
がnil
かどうかに基づいて処理を分岐することです。
もしbase
がnil
であれば、現在のクラスが include される前に実行されるコードブロックを設定します。
既に@_included_block
というインスタンス変数が定義されている場合は、新しいブロックが前に定義されたブロックと異なる場合は、MultipleIncludedBlocks
というエラーを発生させます。
そうでない場合は、@_included_block
に渡されたブロックを代入します。
base
がnil
でない場合は、親クラスのincluded
メソッドを呼び出します(super
を使用)。
これにより、親クラスのincluded
メソッドが実行され、モジュールの include 時の追加の処理が実行されることが期待されます。
つまり、ActiveSupport::Concern により拡張された included メソッドは、引数にブロックを渡すことで呼び出すことができます。
渡されたブロックは、モジュールが include された時に、モジュールを include したクラスやモジュールのコンテキスト(特定のコードやプログラムの実行時の状況や環境)で評価されます。
下記に示したコードが、 included メソッドの使用例です。
module Cancelable
extend ActiveSupport::Concern
included do
has_one :cancellation, class_name:"#{name}Cancellation"
end
end
例えば、次のようなクラスが Cancelable
モジュールを取り込んでいるとします。
class Book
include Cancelable
end
このようにすることで Book モデルに関連づけられた Cancellation オブジェクトを取得できるようになります。
使用例③ クラスメソッドの定義をしやすくするclass_methods
ActiveSupport::Concern では、class_methods
メソッドは 下記のように定義されています。
def class_methods(&class_methods_module_definition)
mod = const_defined?(:ClassMethods, false) ? const_get(:ClassMethods) : const_set(:ClassMethods, Module.new)
mod.module_eval(&class_methods_module_definition)
end
まず、class_methods
メソッドは、ClassMethods
という名前のモジュールがクラス内に既に定義されているかどうかをチェックします。
もし既に定義されていれば、そのモジュールを取得します。もしまだ定義されていなければ、Module.new
を使って新しいClassMethods
モジュールを作成します。
ブロック内のコードをClassMethods
モジュールのコンテキスト(特定のコードやプログラムの実行時の状況や環境)で評価します。
これにより、ブロック内で定義されたクラスメソッドがClassMethods
モジュールに追加されます。
つまり、class_methods
メソッドは、ブロックを引数として受け取り、そのブロック内でクラスメソッドを定義するためのモジュールを作成します。
class_methods
メソッドを使用することで、ブロックに定義したメソッドをクラスメソッドとして、include する側のクラスに取り込むことができます。
下記に示したコードが、class_methods
メソッドの使用例です。
module MyConcern
extend ActiveSupport::Concern
class_methods do
def my_class_method
puts "This is a class method defined in the concern."
end
end
end
class MyClass
include MyConcern
end
MyClass.my_class_method # "This is a class method defined in the concern."
上記の例では、MyConcern
モジュール内でclass_methods
ブロックを使用し、my_class_method
を定義しています。
その後、MyClass
クラスがMyConcern
を include することにより、my_class_method
がMyClass
のクラスメソッドとして利用できるようになります。
使用例④ モジュール間の依存関係の解決
あるモジュールが他のモジュールで定義された変数やメソッドを参照している場合、そのモジュールを利用するクラスでは、両方のモジュールを include する必要があります。
例えば、モジュールAとモジュールAに依存するモジュールB、さらにモジュールBを取り込む(Mix-inする)クラスCがあるとします。コードを書くと下記のようになります。
コード1
module A
def self.included(base)
base.class_eval do
def self.method_injected_by_foo
puts 'モジュールAが実行されました。'
end
end
end
end
module B
def self.included(base)
base.method_injected_by_foo
end
end
class C
include A
include B
end
C.method_injected_by_foo
#=> モジュールAが実行されました。
#=> モジュールAが実行されました。
上記のコードで、私は、モジュールBはモジュールAに依存しているので、モジュールBにモジュールAを include すれば良いと思い、下記のように修正してみました。
コード2
module A
def self.included(base)
base.class_eval do
def self.method_injected_by_foo
puts 'モジュールAが実行されました。'
end
end
end
end
module B
include A
def self.included(base)
base.method_injected_by_foo
end
end
class C
include B
end
C.method_injected_by_foo
#=> undefined method `method_injected_by_foo' for C:Class (NoMethodError)
しかし、上記のコードは正常に実行されず、エラーになります。
理由は、mix-inされるクラスCにモジュールBの依存関係にあるモジュールAが include されていないからです。
mix-in するモジュールが他のモジュールに依存する場合、mix-in される側では両方のモジュールをincludeする必要があります。
しかし、クラスCでモジュールBを使用したいのに、モジュールBが依存するモジュールAもクラスC内に取り込むべきでしょうか?
この問題は、ActiveSupport::Concern を使用することで解決することができます。
上記のコード1 をActiveSupport::Concern を用いて修正した内容が下記のコードです。
module A
extend ActiveSupport::Concern
class_methods do
def method_injected_by_foo
...
end
end
end
module B
extend ActiveSupport::Concern
include A
included do
method_injected_by_foo
end
end
class C
include B
end
上記のようにすることで、モジュール間の依存関係を解決することができます。
使用例⑤ concernメソッドを用いたルーティングの共通化
ルーティングにconcernメソッドを用いることで、ルーティングの共有可能な機能をまとめることができます。これにより、同じ機能を複数の場所で繰り返し記述する必要がなくなります。
使用方法を説明します。
まず、下記のようなルーティングを表すコードがあるとします。
# config/routes.rb
resources :posts do
resources :comments
end
resources :articles do
resources :comments
end
このコードでは、posts と articles の両方のルーティングに comments が設定されています。
これをconcern メソッドを用いることで共通化することができます。
concernメソッド適用後のコードが下記のようになります。
# config/routes.rb
concern :commentable do
resources :comments
end
resources :posts, concerns: :commentable
resources :articles, concerns: :commentable
上記のコードでは、:commentable
という名前の共通の設定をconcern
メソッドを使って定義しています。
その後、resources
メソッドを使用してposts
とarticles
リソースに:commentable
を指定しています。これにより、posts
およびarticles
リソース内で共通の comments のルーティングを生成することができます。
以上が concern メソッドを使用したルーティングの共通化です。
まとめ
今回は、Concern の使い方について紹介してみました。
調べる前は使い方がわかりませんでしたが、いろんな記事やパーフェクト Ruby on Rails という技術書を読み込んでいくうちにConcernの使い方や使用タイミングを理解することができました。
今後は、ModelやControllerを肥大化させないようにするためにConcernをうまく使用していきたいです。
参考
Discussion