Rubyでインスタンス変数によるキャッシュの間違いやすい実装とその改善例
やりたい事
やりたい事として、次の状況を仮定します。
- Ruby でインスタンスメソッドを実行する際に、コストのかかる処理がある
- その処理は複数回実行されるが、1回実行して取得した結果を2回目以降も使いまわしたい(キャッシュしたい)
具体的なコードの例は次になります。
class User
def sample_value
# コストのかかる処理
end
end
user = User.new
p user.sample_value
p user.sample_value
間違いやすい実装例
次は不十分な実装例です。cache_sample1
および cache_sample2
メソッドの2パターンで、インスタンス変数を使ってキャッシュを実装しようとしています。
class User
def cache_sample1
@cache_sample1 ||= sample_value
end
def cache_sample2
if @cache_sample2
@cache_sample2
else
@cache_sample2 = sample_value
end
end
def sample_value
# コストのかかる処理
p "キャッシュを利用していない"
# 値を返す
end
end
user = User.new
p user.cache_sample1
p user.cache_sample1
p user.cache_sample2
p user.cache_sample2
例えば sample_value
メソッドが文字列 Value
を返す場合、実行結果は次になります。
"キャッシュを利用していない"
"Value"
"Value"
"キャッシュを利用していない"
"Value"
"Value"
それぞれ、1回目の呼び出しでは「キャッシュを利用していない
と値 Value
」が出力され、2回目には「値 Value
」のみが出力されているため、キャッシュが実装できています。ただし、これは sample_value
メソッドに依存しています。
問題となるのは、次の様に sample_value
メソッドが偽を返す場合です。
class User
def cache_sample1
@cache_sample1 ||= sample_value
end
def cache_sample2
if @cache_sample2
@cache_sample2
else
@cache_sample2 = sample_value
end
end
def sample_value
p "キャッシュを利用していない"
nil
end
end
user = User.new
p user.cache_sample1
p user.cache_sample1
p user.cache_sample2
p user.cache_sample2
実行結果は次になります。
"キャッシュを利用していない"
nil
"キャッシュを利用していない"
nil
"キャッシュを利用していない"
nil
"キャッシュを利用していない"
nil
呼び出しの都度「キャッシュを利用していない
と値 Value
」が出力されている事から、キャッシュが実現できていない事が確認できます。
これは、1回目の実行で sample_value
の戻り値が @cache_sample1
と @cache_sample2
に代入されるのですが、2回目の実行でキャッシュ済みかを比較した時に、キャッシュされていないと判定されるためです。なぜなら、インスタンス変数の値が偽のためです。
改善例
例えば次のように Object#instance_variable_defined? メソッドを使うと改善します。
class User
def cache_sample3
if instance_variable_defined?(:@cache_sample3)
@cache_sample3
else
@cache_sample3 = sample_nil_value
end
end
def cache_sample4
if instance_variable_defined?(:@cache_sample4)
@cache_sample4
else
@cache_sample4 = sample_present_value
end
end
def sample_nil_value
p "キャッシュを利用していない"
nil
end
def sample_present_value
p "キャッシュを利用していない"
"Value"
end
end
user = User.new
p user.cache_sample3
p user.cache_sample3
p user.cache_sample4
p user.cache_sample4
実行結果は次になります。
"キャッシュを利用していない"
nil
nil
"キャッシュを利用していない"
"Value"
"Value"
これは、1回目の実行で sample_value
の戻り値が @cache_sample1
と @cache_sample2
に代入されると同時にそれらのインスタンス変数が定義されるためです。2回目の実行でキャッシュ済みかを比較した時に、インスタンス変数が定義済みなので、キャッシュされていると判定されます。
Discussion