Ruby学習の一環として、週に1度Ruby関連のブログ記事(英語)を翻訳しています。
今回はRubyのブロックのスコープに関する話。
原文:Watch out for Ruby blocks scope gotcha
↓以下本文↓
Ruby使っているとブロックって便利ですよね。Rubyの柔軟性とパワーはブロックに依るところも大きいです。
ブロックには独自のスコープがあって、クロージャを作るのにも使えます。
これに関連して最近、バグに遭遇しました。その時のコードはだいたいこんな感じ。
def update
availability.update(availability_params)
metrics_tracker = MetricsTracker.build_for(activity)
metrics_tracker.save
if metrics_tracker.trigger_notification?
# send email to admin
end
# rendering code
end
# code omitted...
def metrics_trakcer
@metrics_tracker ||= MetricsTracker.new(current_user)
end
まず、DBトランザクションが抜けているということでバグ発生。なので以下のように追記しました。
def update
ApplicationRecord.transaction do # <= ここにトランザクションを追加
availability.update!(availability_params)
metrics_tracker = MetricsTracker.build_for(activity)
metrics_tracker.save!
end
if metrics_tracker.trigger_notification?
# send email to admin
end
# rendering code
end
しかし、するとadminへのメールが送信されなくなりました……なんで?
ひとまず、トランザクションの箇所を見てみましょう。
ApplicationRecord.transaction do
availability.update!(availability_params)
metrics_tracker = MetricsTracker.build_for(activity)
metrics_tracker.save!
end
metrics_tracker
はブロックの中で定義しています。ここでのmetrics_tracker
はローカル変数で、スコープがブロックの中だけになっています。つまり、ブロック外では、metrics_tracker
は定義されていないことになってしまいました。結果として、コントローラで定義されたmetrics_tracker
メソッドが呼び出されて、availability
については何も知らない新しいオブジェクトが返り値となり、trigger_notification?
にfalse
を返してしまっています。
・・・
もちろん、ローカル変数とメソッド名に同じ名前を使ったのがそもそもの間違いなんですが…
でもまあ、レガシーコードを引き継いで、それでなんとかしなきゃならなくなったとしましょう。
結論、応急処置的にはこういう書き方になりますね。
def update
ApplicationRecord.transaction do
availability.update!(availabiity_params)
@metrics_tracker = MetricsTracker.builde_for(activity) # <= ここを修正!
metrics_tracker.save!
end
if metrics_tracker.trigger_notification?
# send email to admin
end
# rendering code
end
# code omitted...
def metrics_tracker
@metrics_tracker ||= MetricsTracker.new(current_user)
end
ブロックでは扱うオブジェクトを切り替えることができるので、ここではインスタンス変数で区別してみました。こう書けば、metrics_tracker
メソッドはメモ化効果で期待通りの値を返してくれるというわけです。
要するに、ブロックを使うことはオブジェクトを分離するに等しいので、オブジェクトを利用することはできるけどもローカル変数はローカルなままになってしまう、ということです。