September 03, 2005

Ruby extend

Rubyの話です。
Rubyでは機能をあらかじめModuleで宣言しておき、Classに対してextendすることによって、その機能をクラスのスコープとして組み込むことが可能です。例えば

Module A
    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

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

extend_object(obj)はextendのコールバック関数で、例えばClass Aに対してModule BをextendするとB::extend_object(A)という形で起動されます。このことを利用して、instance_evalのところでextendされた側のクラスのクラス変数としてキャッシュの定義、並びにそのクラス変数へのアクセサを提供しています(アクセサを経由しないと遅延評価にならないため)。これで先ほどの例でいうとDB1とDB2でそれぞれにキャッシュが作られるようになり、keyが重複していても問題なくなります。

23:59 fenrir が投稿 : 固定リンク | | このエントリーを含むはてなブックマーク | この記事をdel.icio.usでブックマーク | トラックバック
このエントリーのトラックバックURL: http://fenrir.naruoka.org/mt/mt-tb.cgi/422
コメント
コメントする









名前、アドレスを登録しますか?
(次回以降コメント入力が楽になります)
  • 匿名でのコメントは受け付けておりません。
  • 名前(ハンドル名可)とメールアドレスは必ず入力してください。
  • メールアドレスを表示されたくないときはURLも必ず記入してください。
  • コメント欄でHTMLタグは使用できません。
  • コメント本文に日本語(全角文字)がある程度多く含まれている必要があります。
  • コメント欄内のURLと思われる文字列は自動的にリンクに変換されます。