この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。
こんにちは、shimadaです。
今日は、Rubyの特徴の一つであるブロックを引数にとるメソッドの書き方を説明します。
題材として、「あちこちに同じ順番で処理が並んでいる時のコードの整理」を取り上げたいと思います。
重要な処理を行う場合に、開始、終了、エラーが起こったときの詳細を記録しなければならない。
そんな時の処理は、だいたいこんな風になるのではないでしょうか。
class SimpleWorker
def action(arg)
logger = Application.logger
begin
logger.info("SimpleWorker.action", "start")
do_something
rescue => e
logger.error("SimpleWorker.action", e.message + e.backtrace.join("¥n"))
logger.info("SimpleWorker.action", "abnormal end")
raise
else
logger.info("SimpleWorker.action", "normal end")
end
end
end
class ComplexWorker
def complex_action(arg1, arg2, arg3)
logger = Application.logger
begin
logger.info("ComplexWorker.complex_action", "start")
do_complex_thing1
do_complex_thing2
do_complex_thing3
do_complex_thing4
do_complex_thing5
rescue => e
logger.error("ComplexWorker.complex_action", e.message + e.backtrace.join("¥n"))
logger.info("ComplexWorker.complex_action", "abnormal end")
raise
else
logger.info("ComplexWorker.complex_action", "normal end")
end
end
end
同じような記述がたくさん並んでいてうんざりですね。
気になる点は2つあります。
- “SimpleWorker.action” という文字列が何度も出てきてうざい。
- begin .. rescue .. else .. end のパターンが似ているっていうか同じ
これをリファクタリングしてみましょう。合い言葉は「DRY」です。
1.は変数に入れてしまえばなんとかなりそうです。
2.については、「同じ処理の並びを共有する」というところからGoFで有名なテンプレートメソッドパターンとかを思いつきます。
でも、shimadaは継承が割と嫌いです。
「重要な処理」というのはシステムの色々なところにちょこちょこ出てくるので、「必ず同じ親クラスから派生していなければいけない」という制限をかけるのは、システムを設計する上で大きな足かせになりえるのです。
ではどうするか。Rubyの特色はmix-inです。そしてshimadaの好物は高階関数です。
手続きを引数にとる手続き、つまりブロックを受け取るメソッドをモジュールとして書いてみましょう。
module LogHandler
def logging(task_name)
logger = Application.logger
begin
logger.info(task_name, "start")
yield(logger)
rescue => e
logger.error(task_name, e.message, e.backtrace.join("¥n"))
logger.info(task_name, "abnormal end")
raise
else
logger.info(task_name, "normal end")
else
end
end
end
早速、各クラスにincludeして使ってみます。
class SimpleWorker
include LogHandler
def action(arg)
logging("SimpleWorker.action") do |logger|
do_something
end
end
end
class ComplexWorker
include LogHandler
def complex_action(arg1, arg2, arg3)
logging("ComplexWorker.complex_action") do |logger|
do_complex_thing1
do_complex_thing2
do_complex_thing3
do_complex_thing4
do_complex_thing5
end
end
end
すっきりしましたね。
また、ブロックを使ったプログラミングで混乱しがちなのが、「ブロックが受け取る引数」です。
この例ではyield(logger)に渡された値を do |logger| で受け取っています。
処理の流れが普通の関数呼び出しの逆になるので、初めての人は慣れるまでたくさん素振りして下さい。
ところでブロックを引数にとるメソッドの書き方には何種類かありますが、引数名に ‘&’ をつける方法は、受け取ったブロックを他のメソッドにそのまま渡したい時に便利です。
def method1(obj, &block)
obj.method2(&block)
end
例を書くための例といった感じで、なにをしたいのかさっぱり不明ですが、実際のプロジェクトでまさにこういうコードが実際に必要になったというか先週書いたばかりです。プログラミングって不思議ですね!
ところで、振る舞いのリファクタリング以外でも、ブロック渡しは例えばテストを書くとき等にも役に立ちます。
shimadaがどんな風に使ったかは、また今度の機会に紹介したいと思います。
みなさんもブロックをどんどん使ってRubyを楽しんで下さい。