読者です 読者をやめる 読者になる 読者になる

ウェブサービスを作っています。

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 という便利なメソッドが用意されています。