Ruby extend
Rubyの話です。
Rubyでは機能をあらかじめModuleで宣言しておき、Classに対してextendすることによって、その機能をクラスのスコープとして組み込むことが可能です。例えば
Module A
def hoge; puts hoge; end
end
def hoge; puts hoge; end
end
class B; extend A; end
B::hoge # => "hoge"
ようするにBのクラスメソッドとしてhogeという関数を取り込んでいます。
extendの実践的な使い方として、例えば時間がかかる処理を行うClassに対し、キャッシュの機能をもつModuleを書いて、そのModuleをClassに対してextendすることによってキャッシュを効かせるようにしたいということが考えられます。そこで次のようなコードを書いてみました。
class DB1
@@db = {}
def DB1.get(key); return @@db[key]; end # 時間がかかる処理
end
@@db = {}
def DB1.get(key); return @@db[key]; end # 時間がかかる処理
end
class DB2
@@db = {}
def DB2.get(key); return @@db[key]; end # 時間がかかる処理
end
module CachedDB
@@cache = {}
def get(key); return @@cache[key] || super; end # キャッシュをまず見に行く、なければ親に聞きに行く
end
class DB1; extend CachedDB; end
class DB2; extend CachedDB; end
DB1::get('hoge') #=> CachedDB::get('hoge')
DB2::get('ada') #=> CachedDB::get('ada')
一見これでよさそうに見えます(少なくとも私にはそう見えました…)が、DB1とDB2の中身でkeyが重なってしまうと上手く動きません。というのもCachedDBのスコープで宣言されているクラス変数@@cacheはあくまでもCachedDBのクラス変数であって、extendされた側のDB1やDB2のクラス変数ではないからです。このことに気づくのに時間がかかりました。
解決法は次のとおりです。CachedDBを次のものに入れ替えます。
module CachedDB
def CachedDB.extend_object(obj)
super
obj.instance_eval(<<__EVAL_STRING__)
@cache = {}
def cache; return @cache; end
__EVAL_STRING__
end
def get(key); return cache[key] || super; end
end
def CachedDB.extend_object(obj)
super
obj.instance_eval(<<__EVAL_STRING__)
@cache = {}
def cache; return @cache; end
__EVAL_STRING__
end
def get(key); return cache[key] || super; end
end
extend_object(obj)はextendのコールバック関数で、例えばClass Aに対してModule BをextendするとB::extend_object(A)という形で起動されます。このことを利用して、instance_evalのところでextendされた側のクラスのクラス変数としてキャッシュの定義、並びにそのクラス変数へのアクセサを提供しています(アクセサを経由しないと遅延評価にならないため)。これで先ほどの例でいうとDB1とDB2でそれぞれにキャッシュが作られるようになり、keyが重複していても問題なくなります。
コメント
コメントする
- 匿名でのコメントは受け付けておりません。
- お名前(ハンドル名可)とメールアドレスは必ず入力してください。
- メールアドレスを表示されたくないときはURLも必ず記入してください。
- コメント欄でHTMLタグは使用できません。
- コメント本文に日本語(全角文字)がある程度多く含まれている必要があります。
- コメント欄内のURLと思われる文字列は自動的にリンクに変換されます。
- 投稿ボタンを押してエラーがでなければ、投稿は成功しています。反映されるまでには少し時間がかかります。