詳解Railsデザインパターン:Finderオブジェクト

8 minute read

高尾が解説する 詳解Railsデザインパターン・シリーズの「Finderオブジェクト」編です。

他のデザインパターンも解説していますので、よろしければご覧ください。


Rails の Finderオブジェクト パターンはモデルで処理していたDBのSELECTを専用のクラスに追い出すことでモデルの肥大化を防ぐことに効果があるデザインパターンです。

関連するものとして Queryオブジェクト パターンがあります。Queryオブジェクト パターンはモデルの各 scope に 1 つのクラスが対応していて、シンプルな反面、大量にクラスができたり、コードが重複してしまったりといった問題があります。

それを解消するためのパターンがこの Finderオブジェクト です。

今回はこの Finderオブジェクト パターン を扱います。

Google広告


関連記事・レポジトリ

まずは関連記事の紹介。これらを読めば Finderオブジェクト パターンを理解できます。

ApplicationFinderの実装例

これまでに紹介したパターンとは違い、Finderオブジェクト はブログ記事が少なく、決定的な実装例が見つけられませんでした。
共通するのは

  • あるモデルに対する検索ページを想定して複雑な検索条件を引数で指定できる
  • SomeFinder.search のようにクラスメソッドを提供する
  • ↑のクラスメソッドは複数あるが SomeFinder.published.search のようにメソッドチェーンはできない
それを踏まえた Finderオブジェクト のベースクラスの実装例が以下です。ほぼ [Finder Objects Janko’s Blog](https://janko.io/finder-objects/) の BaseFinder です。

app/finders/application_finder.rb

class ApplicationFinder
  class << self
    def model(klass = nil)
      @model = klass if klass
      @model
    end

    def method_missing(name, *args, **kwargs, &block)
      new(model.all).send(name, *args, **kwargs, &block)
    end
  end

  def initialize(scope)
    @scope = scope
  end

  private

  def scope(new_scope = nil)
    return @scope unless new_scope

    self.class.new(new_scope)
  end

  def arel_table
    self.class.model.arel_table
  end
end

使用例は以下です。これでコントローラーやモデルと独立して複雑な検索処理を MessageFinder に実装できます。

app/finders/message_finder.rb

class MessageFinder < ApplicationFinder
  model Message

  def search(tenant:, user: nil, topic: nil, q: nil)
    messages = with_tenant(tenant)
    messages = scope(messages).with_user(user) if user
    messages = scope(messages).with_topic(topic) if topic
    messages = scope(messages).from_query(q) if q
    messages
  end

  def with_tenant(tenant)
    scope.where(tenant: tenant)
  end

  def with_user(user)
    scope.where(user: user)
  end

  def with_topic(topic)
    scope.where(topic: topic)
  end

  def from_query(q)
    scope.where(arel_table[:content].matches("%#{q}%"))
  end
end

Finderオブジェクト vs Queryオブジェクト

さて、Finderオブジェクト と Queryオブジェクト を解説したところでひとつ疑問が湧きました。これらはどのように使い分ければいいのでしょうか?

これは私個人の考えなのですが

  • Finderオブジェクト は検索ページのみ
  • Queryオブジェクト は複雑な scope
  • 簡単な scope はモデにル直接書く

というのはどうでしょうか。

Finderオブジェクトで定義した with_tenantwith_user は正直使いにくいです。メソッドチェーンができないため scope のほうが便利です。しかしながら、巨大な検索処理をモデルに書くのはメンテナンスがつらいので、Finderオブジェクト に書くといいのではないでしょうか。

また、複雑な scope も同様に Queryオブジェクト に書くと単体テストも書きやすいでしょう。 scope の共通化も実現できます。Queryオブジェクト で定義した scope を Finderオブジェクト で使うとより良いでしょう。

そして、基本的に Rails の仕組みにのっかったほうがメンテナンス性も学習コストも低いので簡単な scope はがんがんモデルに書きましょう。

まとめ

  • Finderオブジェクト パターンは検索ページのような複雑なDBのクエリを扱うときに有効です
  • Finderオブジェクト、Queryオブジェクト、scopeの用途を決めておくといいでしょう

タグ: ,

カテゴリー:

更新日時: