自分の興味の赴くままにIT技術系のネタを取りとめもなくメモっています。
Ruby言語やLinuxのネタが多いです。

April 03, 2010 [長年日記]

[Rails3] ActiveSupport::Concernを自分なりに調べてみた

Rails3の実装を見ていると、とにかくActiveSupport::Concernをextendする、という風になっているらしい。

ってか、ActiveSupport::Concernって何?なんなのこれ? Googleさんに聞いてもあまりまとまった情報がないみたいだし…。

ということで、自分なりに調べてみたのでメモってみる。補足や誤りのご指摘を歓迎します、ぜひ。

まず、ソースコードを見ると…、シンプルなんだけど今のところRDocもなし…orz。

貴重な情報源、現時点で唯一と言っても過言でないブログがこちらにあったのでそちらを参考に勝手な空想を織り交ぜつつ話を進めたいと思う。

まず、目的から。「ActiveSupport::ConcernはRails3のモジュラリティのための重要なツールで、モジュールの依存関係の管理を非常に簡単にかつ直感的にするものです。」とのこと。モジュールに対し、大きく分けて2つの機能を提供する。

ActiveSupport::Concern (1) included do ~ end

included do ~ endの使い方を説明する前に、以下のコードを見てみよう。

%cat test.rb
module Foo
  def self.included(base)
    p "Foo.included is called by #{base}."
  end
end
 
module Bar
  include Foo
  def self.included(base)
    p "Bar.included is called by #{base}."
  end
end
 
puts "ここからアプリケーション------"
class Host
  include Bar
end
Host.new
 
% ruby test.rb
"Foo.included is called by Bar."
ここからアプリケーション------
"Bar.included is called by Host."

Foo/BarはRailsのライブラリ等、Hostクラスがあなたが書くアプリケーションのクラスをイメージするとわかりやすいと思う。で、このHostから見た場合、BarはFooの機能を持っているのでBarだけをincludeしたい、と思うので上記のようなコードになる。大抵はそれで動作するのだけど、self.included()の動作がポイントになる。
先に比較のために同様のコードをActiveSupport::Concernを使って書いてみる。

% cat test2.rb
require 'rubygems'
require 'active_support/concern'
 
module Foo
  extend ActiveSupport::Concern
  included do
    p "Foo.included is called by #{self}"
  end
end
 
module Bar
  extend ActiveSupport::Concern
  include Foo
 
  included do
    p "Bar.included is called by #{self}"
  end
end
 
puts "ここからアプリケーション------"
class Host
  include Bar
end
 
Host.new
% ruby test2.rb
ここからアプリケーション------
"Foo.included is called by Host"
"Bar.included is called by Host"

test.rbとtest2.rbの実行結果から以下の2つの違いが言えると思う。

  1. 前者は呼び出し元(includedの引数)が直前のクラスになるのに対し、後者は呼び出し元(self)がHostになる。
    includedされるタイミングで呼び出し元に何らかの機能を追加するような場合、前者だと全ての呼び出し元クラスとネストされたクラスが連鎖している想定でFoo/Barモジュールを定義しないといけないことがあったりなかったりする。一方、後者は常に一番末端の呼び出し元のクラスを想定するだけで良いので挙動が想定しやすい。
  2. 後者はHostがincludeしたタイミングでFoo, Bar両方のincluded do ~ endブロックが呼び出される。
    Hostクラスの定義時に前者のself.included()の中で何らかの初期化処理を実施しようとした場合、Fooでの初期化処理は結局、Barの機能の一部となる。間接的にはHostクラスでも使えることが多いし実害がない場合も多いが、後者では、直接Hostクラスを初期化できることになるので前者よりもトラブルが少ないはず。

ちなみに、後者の動きをActiveSupport::Concernを使わずにするには以下のように書く。

class Host
  include Foo, Bar
end
% ruby test.rb
"Foo.included is called by Bar."
ここからユーザアプリケーション------
"Bar.included is called by Host."
"Foo.included is called by Host."

HostでFooもBarもincludeする。でも、先祖に遡って全てのモジュールをincludeしなおさなければいけないと言われるとちょっと厳しいよね。あ、それに、この場合でもいったんBarがFooをインクルードしたタイミングでFooが呼ばれてしまうので必ずしもActiveSupport::Concernとは同じにはならないのか。

ActiveSupport::Concern (2) ClassMethods/InstanceMethods

すでにRailsではお馴染みになっている実装だと思うんだけど、以下のように記述すると、クラスメソッド/インスタンスメソッドがそれぞれ定義可能になる。

% cat test3.rb
require 'rubygems'
require 'active_support/concern'
 
module Foo
  extend ActiveSupport::Concern
 
  module ClassMethods
    def test1
      p "test1 is called"
    end
  end
 
  module InstanceMethods
    def test2
      p "test2 is called"
    end
  end
end
 
puts "ここからアプリケーション------"
class Host
  include Foo
end
 
Host.test1
Host.new.test2
% ruby test3.rb
ここからアプリケーション------
"test1 is called"
"test2 is called"

悔しいことに上記の例と同様のことをRubyの標準的な文法(send等を使わずに)で記述するスマートな例が思いつかなかった。結局、includedでClassMethods的なモジュールをincludeしたりextendしたりすることになるのか…(他に良い案があれば教えてくださいませ)

ActiveSupport::Concern (3) まとめ

ActiveSupport::Concernは数十行の小さなモジュールでありながら、標準のRubyでソースを書く場合と書き方(文法というとちょっと語弊があるかな)がだいぶ違う印象があり、馴染んでしまうと元に戻れないと言う意味でかなり破壊力があるモジュールなんじゃないかと思う。

個人的にはこのようにきれいにクラスメソッド/インスタンスメソッドを定義できるとわかりやすいし良いなぁ、なんて思うけど、いかんせん標準のRubyの文法には無いので、Rails以外でActivesupportを使うことに対し心理的な抵抗があるのも事実。心を解き放ってrails信者(どんな場合でもactivesupportは呼ぶのが前提)になるのが一番楽なのかもなー。