Railsで組んでるシステムにCQRSのQ入れたら結構良かった

Railsで組んでるシステムにCQRSのQ層を導入しました。
その事について記載します。

問題点

Q層を導入する前に発生していた問題点について記載します。

戻り値の中身がコード上で判別できない

例えば以下のようなコードです。

class PostRepository
  class << self 
    def get_posts
      Post.joins(:category, :comments)
    end
  end
end

何が戻り値になっているのかをコード上で判別できません。
サンプルのような簡単なコードだと、まだ何とかなるかもしれませんが.
膨大な生クエリの場合もう無理です。

ActiveRecordかhashなのか判別できない

例えば以下のようなコードです。

name = member["name"]

パッとみてhashっぽいですが実はActiveRecordの可能性もあります。
メソッドで呼び出すパターンと文字列で呼び出すパターンの2通りあります。

name = member["name"]
or
name = member.name

コードが膨らんでいる場合、処理を追わないとhashなのかActiveRecordなのか判別できません。

テスト時にmockを使う必要がある

出来る事ならRepositoryをstubする場合の戻り値は実データを使いたいですが
ActiveRecordが戻り値になっているなら難しいです。
minitestでテストを書いている場合はMiniTest::MockでActiveRecordのmockを作って
Repositoryのメソッドにstubする感じになると思います。
外部依存が含まれる場合のみmockにしたいので要素を参照しているだけなら.
可能な限り避けたいです。

改善

Q層を導入して上で挙げた問題点を改善していきます。

Q層の導入

実装は簡単で以下のようにDtoを作るだけです。

class PostQuery
  attr_reader :name, :category, :comment, :title, :content

  def initialize  record
    @name = record["name"]
    @category = record["category"]
    @comment = record["comment"]
    @title = record["title"]
    @content = record["content"]
  end
end

使い方

以下のような形でmapと併用します。

class PostRepository
  class << self 
    def get_posts
      Post.joins(:category, :comments).map{ |m|  PostQuery.new(m) }    
    end
  end
end

これで、Q層に書いたオブジェクトを見に行けば直ぐに中身が判別できるようになります。
テスト時に関してもPostRepositoryからの戻り値をActiveRecordのMockでなく実データで作ることが出来ます。

test_data = [ 
  PostQuery.new({
    "name" => "test_name",
    "category" => "test_category",
    "comment" => "test_comment",
    "title" => "test_title",
    "content" => "test_comment"
  }),
  PostQuery.new({
    "name" => "test_name2",
    "category" => "test_category2",
    "comment" => "test_comment2",
    "title" => "test_title2",
    "content" => "test_comment2"
  }),
]

まとめ

Q層を導入する場合、Repository層からの戻り値を全て調べてDtoとして再実装するコストが発生する。

Q層を取り入れる事によって以下のような恩恵を受けることが出来る。

- テスト実装コストの削減
- 不具合発生時の解決までの時間短縮
- コードの可読性向上
- 副作用が発生するコードの削減

1年以上の長期運用を想定するシステムの場合には
主に保守性の向上からコストを上回る恩恵を受ける事が出来ると考える。