Ruby::クラスインスタンス変数の活躍

Written by @dr_taka_n at 2009/05/16 00:36 []

Rails を触り始めるまではあまり意識していなかった Ruby のクラスインスタンス変数。

「インスタンス変数」、「クラス変数」はメジャーなので言うまでもないのだが、 「クラスインスタンス変数」と言ったところで何だそれ?という感じだった。。

どのような使われ方がされているのか?

いろいろな例があるが、例えば、ある日、Rails のコントローラで、コントローラ毎に CRUD 処理の決まりきった同じ処理を何度も書きたくなくなったとする。

そう、これは、以前紹介文を書いた Rails Plugin::ResourceController などのプラグインを使用すればで実現できるのだが、仮にそういうものがなかった時期に自分でこのような機能を追加するとした場合、どうするか?

決まりきった同じ処理を書かないで済むようになるコードを super クラスに書いて継承させる、もしくは Module として定義して Mix-In させることになるのだが、 まずモデルをどのように保持するか?となった時に一瞬考える。
(実際に例に挙げた機能を実装するには、いろいろなことを考えないといけないが、本稿ではクラスインスタンス変数についての話なので、ここではこのモデルの保持のことについてだけフォーカスする)

コントローラクラスの定義時に、関連するモデルをクラスメソッドを使用して定義しておき、コントローラクラスで利用できるようにする必要がある。

class EntryController < ResourceController
  model_class Entry
end

のような感じ。

この手の手法は、Rails ではよく出てくるイディオム的なものだが、ここでクラスインスタンス変数が活躍している。 上記の場合、ResourceController は、Entry というモデルの情報を覚えておく必要がある。 ResourceController ではそれを ResourceController クラスのクラスインスタンス変数で記憶する。

クラス自体もオブジェクトであるので、そのクラスに紐つくインスタンス変数を持てる。それがクラスインスタンス変数であるのだが、

  • クラス変数との違いは何になるのだろうか?
  • クラス変数では何が問題になるのか?

というところが最初にクラスインスタンス変数を見たときに考えたところだった。

この部分の説明については、Martin Fowler 氏の書いた

が非常にわかり易い。

以下、上記のページを引用しつつ、書いてみる。

class Employee
  @@instances = []
  def self.instances
    return @@instances
  end
  def store
    @@instances << self
  end
  def initialize name
    @name = name
  end
end

Employee.new('Martin').store
Employee.new('Roy').store
Employee.new('Erik').store

puts Employee.instances.size

上記は、MF Bliki: ClassInstanceVariable より引用

最後の puts は 3 を返す。問題ない。 クラス変数は、クラス特異の変数であるので、そのクラスからインスタンス化されたオブジェクトにおいても共通の参照先を持ち続ける。

さて、この次のコードで、先の問いかけ「クラス変数では何が問題になるのか?」の解がある。

class Employee
  @@instances = []
  def self.instances
    @@instances
  end
  def store
    @@instances << self
  end
  def initialize name
    @name = name
  end
end

class Programmer < Employee; end
class Overhead < Employee; end

Overhead.new('Martin').store
Overhead.new('Roy').store
Programmer.new('Erik').store

puts Overhead.instances.size
puts Programmer.instances.size

上記は、MF Bliki: ClassInstanceVariable より引用

先程と異なる点としては、継承が入っているところだ。 最後の puts の結果はどうなるか?結果は、両方とも 3 となる。

上記コードで結果として期待しているのは、2 と 1 が返る結果だろう。 そう、クラス変数は継承してしまうのだ。これはありがたくない。

求める結果を実現するためには、 MF Bliki: ClassInstanceVariable にて紹介されている Hash を使う方法もある。

更に Ruby では、クラスインスタンス変数を使う手がある。これが Rails などでよく使われている手法だ。

class Employee
  class << self; attr_accessor :instances; end
  def store
    self.class.instances ||= []
    self.class.instances << self
  end
  def initialize name
    @name = name
  end
end

class Overhead < Employee; end
class Programmer < Employee; end

Overhead.new('Martin').store
Overhead.new('Roy').store
Programmer.new('Erik').store
puts Overhead.instances.size
puts Programmer.instances.size

上記は、MF Bliki: ClassInstanceVariable より引用

最後の puts の結果は、期待通り、2 と 1 を返す。

上記のコードの中で何が行われているのか?

2行目の class << self はこれもよく使われる特異(singleton)クラスを書き始める時のイディオムだ。

self は自分自身を返す。では、”自分自身”が誰になるのか?がポイントとなる。

上記の Employee クラスの中の selfEmployee を返す。 Employee を継承した Overhead クラスでは、selfOverhead を返す。 同じ Employee を継承した Programmer クラスでは、Programmer を返す。 それぞれの特異クラスに対してattr_accessorでインスタンス変数を設定しているので、 そのインスタンス変数は Overhead クラス、Programmer クラス毎に管理される。

そのため、上記の結果は期待通りとなっている。

これを先のResourceControllerの話に戻すと、例えば Radiant の

  • app/controllers/admin/resource_controller.rb

では、以下のような書き方になっている。

class Admin::ResourceController < ApplicationController
  extend Radiant::ResourceResponses
  ...

  def self.model_class(model_class = nil)
    @model_class ||= (model_class || self.controller_name).to_s.singularize.camelize.constantize
  end

特異メソッドを使用して、クラスインスタンス変数を設定している。 この表現は、クラスインスタンス変数に慣れていないと、パッと見、一瞬悩むかもしれない。 「特異メソッドの中でインスタンス変数?」と。。

上記の表現で、モデルの情報を Admin::ResourceController に保持できた。では、情報を保持したのはいいが、このクラスインスタンス変数にはどのようにアクセスするのか?

  protected
    ...
    def model
      instance_variable_get("@#{model_symbol}") || load_model
    end
    ...
    def model=(object)
      instance_variable_set("@#{model_symbol}", object)
    end

先の Martin Fowler 氏の例と違い、アクセサは書いていないので、 Object#instance_variable_get/set を使用して、先に保持した @model_class クラスインスタンス変数にアクセサし、 model/model=(object) メソッドにてアクセサを用意してあげる必要がある。

クラスインスタンス変数の本題とは外れるが、上記の model_symbol の値は以下の流れで出てくる。

  protected
    ...
    def model_class
      self.class.model_class
    end
    ...
    def model_name
      model_class.name
    end
    ...
    def model_symbol
      model_name.underscore.intern
    end

ちなみに、model_class メソッドの中の self.class は、自分自身のクラスを返す。 以下のような Admin::ResourceController を継承した EntryController があるとすると、

class EntryController < Admin::ResourceController
end

この場合、model_class 内で定義されている selfEntryController になり、sefl.class は、EntryController が参照するクラス Admin::ResourceController になる。 なので、model_class のレシーバは Admin::ResourceController になり、先に出てきた def self.model_class(model_class = nil) のメソッドが呼ばれることになる。既にクラスインスタンス変数が設定されていれば、それが返ることになる。

事前の予備知識がないと、少々混乱してしまうところでもあるが、このあたりは、Ruby の楽しいところでもある。

参考サイト

参考本::実践 Rails -強力なWebアプリケーションをすばやく構築するテクニック

第1章が「基本的な手法」となっているのだが、「基本」と言いつつ、とても基本とは言えないレベルの内容になっている。。 「1.2.2 メソッド参照」の説明などは非常にうまいものだった。 この項で図を含めてメソッドの参照ルールが以下の順に説明される。

  • 基本ルール
  • クラスの継承
  • クラスのインスタンス化
  • モジュールのインクルード
  • 特異クラス
  • クラスオブジェクトの特異クラス

この項を読んで初めてピンときた気がする。

実践 Rails -強力なWebアプリケーションをすばやく構築するテクニック
Brad Ediger
オライリージャパン
売り上げランキング: 58569
おすすめ度の平均: 3.5
2 訳が残念すぎます
5 「Railsの外側」の問題解決の宝庫
blog comments powered by Disqus