リポジトリ(Repository)パターン(PofEAA)

ドメインオブジェクトの検索と永続化機能を抽象化して提供するパターン。バックグラウンドにRDBなどのデータソースがあり、内部的にデータソースから受け取ったデータをドメインオブジェクトにして返すということを行う。

バックグラウンドのデータソースはStrategyパターンを適用することで、切り替えることができる。

リポジトリを使うことで、データソースからのドメインオブジェクト構築とデータソースへのドメインオブジェクト永続化をカプセル化することができ、リポジトリの外側ではドメインオブジェクトを扱うことに集中できるようになる。

class EmployeeRepository
  # idにマッチするEmployeeを返す
  def find(employee_id)
  end

  # 条件にマッチする最初のEmployeeを返す
  def find_by(criteria)
  end

  # 条件にマッチするすべてのEmployeeを返す
  def where(criteria)
  end

  # 指定のEmployeeを保存する
  def save(employee)
  end
end

EmployeeRepository#whereを使えば(詳しいことは知らないが)条件にマッチするEmployeeを手に入れることができるし、EmployeeRepository#saveを使えば(やはり詳しいことは知らないが)Employeeを永続化することができる。

もし、Employeeが複数のドメインオブジェクトへの関連を持っていて、それらすべてを永続化したりデータソースから復元しなければならないとしても、リポジトリの内側で処理されるため、使う側は気にする必要がない。

Employeeが社員とその家族の情報を持っているときの、EmployeeRepositoryの具体的な例を示す。

class FamilyMember
  attr_reader :id, :name
  def initialize(id, name)
    @id = id
    @name = name
  end
end

# taro = FamilyMember.new(1, '太郎')
# hanako = FamilyMember.new(2, '花子')
# Employee.new(1, '山田二郎', [taro, hanako])
class Employee
  attr_reader :id, :name, :family_members
  def initialize(id, name, :family_members)
    @id = id
    @name = name
    @family_members = family_members
  end
end

# MemberクラスはRailsのActiveRecordクラス
class EmployeeRepository
  def find(employee_id)
    member = Member.find(employee_id)
    # ドメインモデルではFamilyMemberだがRDB上はDependent
    family_members = member.dependents.map do |family_member|
      FamilyMember.new(family_member.id, family_member.name)
    end
    Employee.new(member.id, member.name, family_members)
  end

  def save(employee)
    ApplicationRecord.transaction do
      member = Member.find(employee.id)
      member.name = employee.name
      # 本題ではないので、増減があった場合などは考慮していない
      employee.family_members.each do |family_member|
        member.dependents.each do |dependent|
          if dependent.id == family_member.id
            dependent.name = family_member.name
            dependent.save!
          end
        end
      end
      member.save!
    end
  end
end

EmployeeRepositoryは大半のコードがドメインオブジェクトとデータソースの付き合わせに費やされており、付き合わせを単純化する仕組みが使えるなら使うのもありかもしれない。個人的には、そういうツールのようなものは痒い所に手が届かないことが多いため、使いたくないという気持ちが強い。十分な検証を行った上で、8割は適切に機能し残り2割もツールを使わずに簡単に記述できると結論付けられる場合には使っても良いとは思うが。

データソースが異なる場合、上記の付き合わせの方法も変わるが、検索方法も変わるはずである。具体例をあげれば、ActiveRecordではUser.where('id > ?', num)だがMongoidではUser.where(:id.gt => num)となるといった変更が必要になる。しかし、実際に行っていることは「指定した値より大きいidのuserを取得する」であり抽象化が可能である。この条件を抽象化するパターンとして仕様(Specification)がある。とはいえ、どちらにせよ仕様パターンから条件を取得して、それぞれのデータソースに対する問い合わせの形式で組み立てるレイヤを構築しなければならない。

付き合わせの問題も検索方法の問題も、ActiveRecordでもMongoidでも動くか切り替えが容易であることが求められるシステムの場合に抱える問題であって、「ActiveRecordで十分でMongoidにするかなんてわからないよ。Mongoid使うことになったらなんとかするさ」というシステムなら無用な拡張性になる。個人的には、ActiveRecordやMongoidを決め打ちで使うシステムなら、サービスレイヤ(ServiceLayer)パターンに直接検索ロジックを書いてしまって、ドメインオブジェクトとデータソースの付き合わせはActiveRecordやMongoidのクラスにメソッドを用意してしまう方法もありだと考えている。決め打ちなのに、リポジトリと仕様を適用してActiveRecordやMongoidを隠しても屋上屋を架すだけにしかならないだろう。

外部APIを利用するようなケースの場合でも、ゲートウェイ(Gateway)パターンを用意して同じようにドメインオブジェクトとデータソースの付き合わせメソッドを用意してしまえば良いと考える。

ActiveRecordやMongoidのクラスやゲートウェイにドメインオブジェクトとデータソースの付き合わせメソッドがあるなら、それはリポジトリそのものではないかと思えるが、実際には異なる。あくまで、リポジトリはドメインオブジェクトに対する検索や永続化を担うのであり、ActiveRecordやMongoidのクラスやゲートウェイは関連する外部リソースに対する検索や永続化を担うという違いがある。そのため、リポジトリはドメインオブジェクトか集約(Aggregate)の名前をつけたクラス名になり、モジュール構造もドメインモデル(DomainModel)パターンのモジュール構造と一致させる方が良いだろうと考えている。

拡張性が不要ならリポジトリを作らなくても良いと考えているとすでに述べている通り、DDDを実践するに当たってリポジトリは必ず作らなければならない存在とは限らないと考えている。必要なのは「ドメインオブジェクトの検索と永続化機能」であり、クラスを増やしたり複雑化せずに単純に実現できるならその方法を使うべきだろう。