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
クラスの中の self
は Employee
を返す。
Employee
を継承した Overhead
クラスでは、self
は Overhead
を返す。
同じ 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
内で定義されている self
は EntryController
になり、sefl.class
は、EntryController
が参照するクラス Admin::ResourceController
になる。
なので、model_class
のレシーバは Admin::ResourceController
になり、先に出てきた
def self.model_class(model_class = nil)
のメソッドが呼ばれることになる。既にクラスインスタンス変数が設定されていれば、それが返ることになる。
事前の予備知識がないと、少々混乱してしまうところでもあるが、このあたりは、Ruby の楽しいところでもある。
参考サイト
参考本::実践 Rails -強力な Web アプリケーションをすばやく構築するテクニック
第 1 章が「基本的な手法」となっているのだが、「基本」と言いつつ、とても基本とは言えないレベルの内容になっている。。 「1.2.2 メソッド参照」の説明などは非常にうまいものだった。 この項で図を含めてメソッドの参照ルールが以下の順に説明される。
- 基本ルール
- クラスの継承
- クラスのインスタンス化
- モジュールのインクルード
- 特異クラス
- クラスオブジェクトの特異クラス
この項を読んで初めてピンときた気がする。
オライリージャパン
売り上げランキング: 58569
「Railsの外側」の問題解決の宝庫