LoginSignup
3
2

More than 3 years have passed since last update.

そのまま書くには違和感のあるロジックはクラスに分離しよう〜毎週○曜日に実行されるRakeタスクの実装を例に(Heroku)

Posted at

問題

Herokuでスケジュール実行のために用意されているアドオンHeroku Schedulerでは「10分毎」「1時間毎」「1日毎」のオプションが用意されているものの、「1週間毎」に実行したいときのオプションはないので、週次でタスクを実行したい場合はRails側で実装する必要があります。さて、それでは週次で実行するための判定処理はどこに書くべきでしょうか?

よくあるコード

こんなときよく見かけるのがパターンAとパターンBのコードですが、双方ともデメリットが大きいです。

パターンA: Rakeタスク側に判定コードを書く

DAY_OF_WEEK = %i[sun mon tue wed thu fri sat].freeze

namespace :awesome_check do
  desc 'Invoke AwesomeChecker'
  task run: :environment do
    if DAY_OF_WEEK[Date.current.wday] == :sat
      AwesomeChecker.call
    end
  end
end

シンプルではありますが、Rakeタスクのテストは書きづらいので、このコードの例で言えば本当に土曜日だけに実行されるように担保したい場合に困ります。

【参考】Rakeタスクのテストを書きたい人はこちらの記事が参考になります
RailsでRakeタスクをシンプルかつ効果的にテストする手法

パターンB: AwesomeChecker側に判定コードを書く

class AwesomeChecker
  # ...(中略)...
  def call
    return false unless DAY_OF_WEEK[Date.current.wday] == :sat

    # (処理の中身を書く)
  end
  # ...(中略)...
end

「週次実行するビジネスロジック」と「AwesomeCheckerのそもそものビジネスロジック」は目的が異なるので、コードとしていびつです。そもそも、所定の曜日にしか実行できない関数が存在するのは不気味ですよね。

解決策: 週次実行を管理するクラスを作る

どちらにロジックを書いても不自然になるのであれば、新しくクラスを作ってロジックを分離するのが策になります。Rakeタスクにこんな感じのコードを書けるとすれば、Rakeタスクにロジックを書かなくても済むし、AwesomeChecker上に週次実行のロジックを盛り込まなくても済みそうです。

namespace :awesome_check do
  desc 'Invoke AwesomeChecker'
  task run: :environment do
    # NOTE:
    # Procで実行したいコードを渡しておけば、
    # 即座に実行されてしまうことはありません
    DayOfWeekInvoker.call(:sat, -> { AwesomeChecker.call })
  end
end

このような「指定された曜日にだけ第二引数の関数が実行される」クラスの実装はこんな感じになるかと思います。

class DayOfWeekInvoker
  DAY_OF_WEEK = %i[sun mon tue wed thu fri sat].freeze

  def self.call(*args)
    new(*args).call
  end

  def initialize(day_of_week, func)
    @day_of_week = day_of_week
    @func = func
    unless valid_day_of_week?
      raise ArgumentError, "無効な引数です: #{day_of_week}"
    end
  end

  def call
    unless DAY_OF_WEEK[Date.current.wday] == @day_of_week
      Rails.logger.info '[DayOfWeekInvoker] Invocation Skipped'
      return
    end
    @func.call
  end

  private

    def valid_day_of_week?
      @day_of_week.in? DAY_OF_WEEK
    end
end

Heroku Schedulerのオプションで「日毎」が選択できるため、1日1回だけ実行されるというロジックはHeroku Schedulerにお任せしている実装にはなりますが、1日1回だけ実行されることをRails側で担保したい場合もこのような発想でクラスを分離すればテストも簡単になります。

「何だかロジックが入り組んでテストを書きづらくなったなー」というときにご参考ください。

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2