ActiveSupport の ClassInheritableAttributes に関して
以下、Ruby 1.8 での話です。
ActiveSupport の機能で ClassInheritableAttributes というものがあります。
これは、継承するクラス変数のようなものです。
クラス変数の問題点
クラス変数は、親クラスに同名のものが存在する場合、子クラスでの変更が親クラスはもちろん、親が同じの他の子クラスにも影響します。
つまり、
class Mother @@base_age = 50 def add_age(age) @@base_age + age end end class Son < Mother @@base_age = 30 end class Daughter < Mother @@base_age = 20 end
とした場合、
Mother.new.add_age(1) # => 51 Son.new.add_age(1) # => 31 Daughter.new.add_age(1) # => 21
を期待するかもしれませんが、実際には
Mother.new.add_age(1) # => 21 Son.new.add_age(1) # => 21 Daughter.new.add_age(1) # => 21
となります。最後に反映される Daughter クラスの @@base_age = 20 が、共有されてしまうのです。
クラス変数は、親クラスに同名のものが存在する場合、クラス内で共有されるグローバル変数のようなものであることを強く意識する必要があります。
そこで ClassInheritableAttributes
クラス変数のようなものは欲しい、しかし別の子クラスでも共有されてしまうのは困る、というケースは結構あると思います。
そこで ActiveSupport の ClassInheritableAttributes の出番です。
まさに、継承するクラス変数のようなものを使えるようになります。
先ほどの例を ClassInheritableAttributes で書き直してみます。
require 'rubygems' require 'active_support' class Mother class_inheritable_accessor :base_age self.base_age = 50 def add_age(age) base_age + age end end class Son < Mother self.base_age = 30 end class Daughter < Mother self.base_age = 20 end Mother.new.add_age(1) # => 51 Son.new.add_age(1) # => 31 Daughter.new.add_age(1) # => 21
期待通りの結果となりました。
Class#class_inheritable_accessor メソッドを使うと、引数で指定した名前のアクセサがクラスメソッド・インスタンスメソッドとして登録され、ClassInheritableAttributes を使えるようになります。
今回は base_age 1 個しか定義しませんでしたが attr_accessor のように、引数にはシンボルを並べることができます。
class 〜 end の中で self.base_age= のように self. をつけている理由は、つけないと class 〜 end 内のローカル変数になってしまうからです。
ClassInheritableAttributes とは何なのか
ところで、この黒魔術の実体は何なのでしょうか。
上の Mother クラスを class_inheritable_accessor なしで書き換えると、だいたい以下と同じになります。
class Mother #== 動的に生成される部分 == def self.base_age @base_age end def self.base_age=(val) @base_age = val end def base_age self.class.base_age end def base_age=(val) self.class.base_age = val end #========================== self.base_age = 50 def add_age(age) base_age + age end end class Class private def inherited_with_inheritable_attributes(child) child.instance_variable_set '@base_age', @base_age end alias inherited_without_inheritable_attributes inherited alias inherited inherited_with_inheritable_attributes end
まずは Mother クラスだけ見てください。class_inheritable_accessor の実体は、クラスのインスタンス変数を操作しています。
クラスのインスタンス変数は通常、インスタンスメソッドから操作できません。
これをクラスメソッド・インスタンスメソッドで同じように操作できるようにしているわけです。
ところで、クラスのインスタンス変数は継承されません。(参考)
そこで、クラスが継承されたときに呼び出される Class#inherited メソッドを拡張し、親クラスのインスタンス変数を子クラスにコピーしてやります (Class#inherited_with_inheritable_attributes の部分)。
上のコードは割といい加減なので、実際の class_inheritable_accessor は、もう少し厳密に処理されています。
興味を持ったら
ClassInheritableAttributes は activesupport-2.3.8/lib/active_support/core_ext/class/inheritable_attributes.rb がソースです。
詳しく調べたいときは、このソースを読むと良いと思います。
ハッシュや配列を ClassInheritableAttributes で使いたいときは class_inheritable_hash, class_inheritable_array という便利なメソッドが用意されています。