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として定義しておけば済むのだけど、 こうしておくことのメリットとしてこの場合は、ActiveRecord(MySQL)に依存しない処理なども 流れるようなインターフェイスで問い合わせ処理が可能になったり、Specificationクラスから条件を満たす データが簡単に引き出せるようになることだとは思う。
DDD詳しくないので、ほんとうに良いかどうかわからないけど、メソッドチェーン自体は気持ちいので、 ActiveRecordに依存したくない気持ちになった時にはこのような書き方を試したりしてみる。
良い設計目指して歯を食いしばらせましょう。