2018年9月3日月曜日

Rubyでメモ化するときの話

あるクラスに以下のような自身が持つemailAccountを検索して返すメソッドがあるとしましょう。

def account
  Account.find_by(email: self.email)
end

このメソッドには問題があります。
呼び出しのたびに検索処理が行われてしまうことです。

解決するためにはどうするでしょうか。
解決方法はいくつかありますが、ここでは以下のようにメモ化するという選択をしたとしましょう。

def account
  @account ||= Account.find_by(email: self.email)
end

これで初回の呼び出しのみで検索が行われ、2回目以降はインスタンス変数に設定された値が使われるようになります。
よし、完璧だ、と思ったでしょうか。

しかしこれでもまだ問題があります。
検索処理がnilを返す可能性です。

検索処理でnilが返らない前提ならば、以下のように例外が発生するfind_by!のようなメソッドを使用しておけばヒットしないケースは例外的ケースのみということで問題にはならないでしょう。

def account
  @account ||= Account.find_by!(email: self.email)
end

ここでは検索処理でnilが返ることがある前提で話を進めましょう。
nilが返った場合に何が問題か。

検索処理でnilが返る場合、accountメソッドが呼ばれるたびに検索処理を行ってしまうのです。
それもそのはず、Rubyの||=演算子は左辺が偽の場合に右辺の処理行い左辺に代入する演算子だからです。
Rubyで偽の場合というのはnilfalseの場合です。
Rubyのインスタンス変数は未初期化であればnilとして扱われます。
初回の実行時はこれにより右辺が評価され、インスタンス変数に値が設定されます。
右辺が偽を返さない場合は2度目以降は左辺の評価が真となりますので右辺が評価されることなく終わりますが、右辺が偽の値を返していた場合は2度目以降でも左辺の評価が偽となり右辺の評価が行われてしまいます。
これは望んでいる挙動ではないことでしょう。

それではどうするか。
こういう場合は以下のようにinstance_variable_defined?を使用します。

def account
  return @account if instance_variable_defined? :@account
  @account = Account.find_by(email: self.email)
end

これならばインスタンス変数が未定義の場合(つまり初回呼び出しの場合です)は、検索処理が行われその結果が何であろうともインスタンス変数に代入されます。
2度目以降の呼び出しでは設定された値がnilであってもその値が返されます。
つまり検索処理は1度しか行われません。

これで望んでいた動作になりました。
良かった。

2 件のコメント: