Ruby
Rails

Rubyのクラスでキャッシュ処理を実現する方法

はじめに

RailsでWebアプリを開発していて、ファイルのデータを読み込み、そのなかから必要な値を返すようなメソッドがあるとする。
何度も呼ばれる場合は、ファイルのデータを毎度読み込むのはコストが高いためメモリ上に保存しておきたい。
そんな時にプログラムを書けばよいか、方式はいろいろあるが簡易的に変数に格納した値を再利用する方法を検討した。

クラスメソッドやインスタンスメソッド、クラス変数やインスタンス変数を利用した方法について検証したコードを書いてみる。

結論から書くとブラウザからRailsを通してアクセスする場合は、クラス変数を使ってインスタンスメソッドで呼び出すのが適当だ。

サンプルコードでやること

  • 変数にハッシュを用意する。
  • メソッドからキー値を入力とし、
  • ハッシュに値があれば値を返し、
  • なければ"val"という文字列を入力値のキーにしてハッシュに格納する
  • インスタンスメソッドと、クラスメソッドの場合でそれぞれ試している

クラス変数を利用したサンプルコード

class Aaa
  @@hash = {}

  def instance_method(key)
    if @@hash[key]
      p "キャッシュを利用しました"
      @@hash[key]
    else
      p "格納しました"
      @@hash[key] = "val"
    end
  end

  def self.class_method(key)
    if @@hash[key]
      p "キャッシュを利用しました"
      @@hash[key]
    else
      @@hash[key] = "val"
      "格納しました"
    end
  end
end

インスタンスメソッドの実行例

クラスをインスタンス化すればキャッシュを利用できる

aaa = Aaa.new
=> #<Aaa:0x007ff5e06edba0>
> aaa.instance_method(:bar)
"格納しました"
=> "val"
> aaa.instance_method(:bar)
"キャッシュを利用しました"
=> "val"

クラスメソッドの実行例

クラスメソッドでもキャッシュを利用できる

> Aaa.class_method(:foo)
=> "格納しました"
> Aaa.class_method(:foo)
"キャッシュを利用しました"
=> "val"

インスタンス変数を利用したサンプルコード

class Bbb
  def initialize
    @hash = {}
  end

  def instance_method(key)
    if @hash[key]
      p "キャッシュを利用しました"
      @hash[key]
    else
      p "格納しました"
      @hash[key] = "val"
    end
  end

  def self.class_method(key)
    if @hash[key]
      p "キャッシュを利用しました"
      @hash[key]
    else
      @hash[key] = "val"
      "格納しました"
    end
  end
end

インスタンスメソッドの実行例

クラスをインスタンス化すればキャッシュを利用できる

bbb = Bbb.new
=> #<Bbb:0x007ff5e54c6a70 @hash={}>
> bbb.instance_method(:foo)
"格納しました"
=> "val"
> bbb.instance_method(:foo)
"キャッシュを利用しました"
=> "val"

クラスメソッドの実行例

@hashはクラスメソッドのスコープが外れているため見えない。雑に言うと存在してないのでnil扱いになってるというわけでだ

Bbb.class_method(:foo)
NoMethodError: undefined method `[]' for nil:NilClass

検証結果よりインスタンス化すれば、何度もキャッシュを呼び出すことはできる。
しかしRailsのコントローラから呼ばれた場合は都度新しくインスタンス化する必要があり、キャッシュすることができない。
なのでこの仕組でキャッシュ機能を作る場合は、クラス変数を利用したクラスを利用してキャッシュ機能は作るべきである。

クラス変数を利用したサンプルコードをもう少しRubyっぽく書く

もうちょっとRubyっぽく書いた class_method2を作る
||= でnilガード

この辺のキャッシュ機能の書き方は, いろいろなgemで使われている。
先日見たコードだとこの祝日を返すgemでも使われていた(https://github.com/holiday-jp/holiday_jp-ruby/blob/master/lib/holiday_jp.rb#L14)

class Ccc
  @@hash = {}
  def self.class_method(key)
    if @@hash[key]
      p "キャッシュを利用しました"
      @@hash[key]
    else
      @@hash[key] = "val"
      "格納しました"
    end
  end

  def self.class_method2(key)
    p @@hash[key] ? "キャッシュを利用しました" : "格納しました"
    @@hash[key] ||= "val"
  end
end
Ccc.class_method2(:foo)
"格納しました"
=> "val"
> Ccc.class_method2(:foo)
"キャッシュを利用しました"
=> "val"

※ クラスインスタンス変数はややこしくなるので今回は対象にしていない
クラス変数とクラスインスタンス変数