その他
    ホーム 技術発信 DoRuby ブロックを使ったリファクタリング

    ブロックを使ったリファクタリング

    この記事はアピリッツの技術ブログ「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つあります。

    1. “SimpleWorker.action” という文字列が何度も出てきてうざい。
    2. 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を楽しんで下さい。