読者です 読者をやめる 読者になる 読者になる

Rubyでメソッドチェイン可能なクエリオブジェクトを書く

ひとでさんのこの前のYAPCでの発表(ベストスピーカーおめでとうございます)の はてなブログではドメイン駆動など使ってイケてるサービスを作り続けているぜっていう話、 オブジェクト指向やらドメイン駆動の話とかいろいろあったけれども、 結局のところイケてるサービスを作り続けるためには継続的にコードベースも良くしていかなければいけない っていうメッセージがあったのだったのだと勝手に解釈していてすごい共感している。

エリック・エヴァンスの本自体買って中途半端に読んで読みっぱなしなので、 ドメイン駆動設計については何も意見できるところはまだない。

でも最近思うのが、複雑な仕様を手続き的に実装すると確実に複雑なままになってしまうので、 やっぱりビジネス上に現れるルールは単語で表現してコードに起こすという作業をしないと ダメだと思って、ここ数日過去に作ったコードに手を加えるついでに大幅に書き直している。

まだちゃんと身についたわけではないけど、エヴァンス本読み返している中見つけた、 Specificationパターンが素敵だなと思っていて今実装している複雑な仕様の実装に活用しようとしている。

Specificationパターンを利用しようとすると、ActiveRecordに直に依存しないレイヤーとして Repositoryを実装しないといけないなぁとなって思ってこれをRailsの中に取り込んでいくとどうなるか考え、以下のようなシンプルなライブラリを書いた。

module ChainableQuery
  def initialize(init_value, filters = [])
    @init_value = init_value
    @filters = filters.dup.freeze
  end

  def add_filter(&block)
    self.class.new(@init_value, [*@filters, block])
  end

  def value
    @filters.reduce(@init_value) { |last_value, filter| filter.call(last_value) }
  end
end

これ便利で、簡単に遅延評価されてメソッドチェーン出来る汎用的なクエリオブジェクトが作れる。 例えばUserクラスの絞り込み処理などを持ったRepositoryを定義しようとすると以下のように書ける。

class UserRepository
  def initialize(scope = User)
    @scope
  end

  def query
    Query.new(@scope)
  end

  def find_piad_premium_user
    query.and_premium.and_paid.value
  end

  class Query
    include ::ChainableQuery

    def and_premium
      add_filter { |query| query.where(is_premium: true) }
    end

    def and_paid
      add_filter { |query| query.select { |user| user.payments.exists? }  }
    end

    def and_has_profile_image
      add_filter { |query| query.where("profile_image_url IS NOT NULL") }
    end
  end
end

repository = UserRepository.new
premium_users = repository.find_paid_premium_users

サンプルはすごい適当だけど、初月無料やクーポンの利用などのせいで実際にまだ課金したことがないユーザではなく、 何らかの決済手段で課金したことが有るようなユーザのオブジェクトを取得する処理が↑のように、 メソッドチェインを利用したクエリオブジェクトを通してメソッドが書けるはず。

RailsだったらこういうのはだいたいScopeとして定義しておけば済むのだけど、 こうしておくことのメリットとしてこの場合は、ActiveRecordMySQL)に依存しない処理なども 流れるようなインターフェイスで問い合わせ処理が可能になったり、Specificationクラスから条件を満たす データが簡単に引き出せるようになることだとは思う。

DDD詳しくないので、ほんとうに良いかどうかわからないけど、メソッドチェーン自体は気持ちいので、 ActiveRecordに依存したくない気持ちになった時にはこのような書き方を試したりしてみる。

良い設計目指して歯を食いしばらせましょう。